<?php /*

 ocPortal
 Copyright (c) ocProducts, 2004-2012

 See text/EN/licence.txt for full licencing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/*EXTRA FUNCTIONS: glob*/

/**
 * @license		http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright	ocProducts Ltd
 * @package		core_database_drivers
 */

/*
	Known (intentional) issues in SQL support (we are targetting MySQL-4.0 compatibility, similar to SQL-92)
		We support a few MySQL functions: REPLACE, LENGTH, CONCAT. These are not likely usable on all DB's.
		We do not support the range of standard SQL functions.
		We do not support SQL data types or CAST, we use ocPortal ones instead. We don't support complex type-specific ops such as "+" for string concatenation.
		HAVING is not supported
		We do not support SQL functions (COUNT etc) outside of a SELECT clause
		We do not have any special table/field naming escaping support-- so you need to use names that aren't awkward
		MySQL-style auto-increment is supported, but actually done as key randomisation, once install has finished
		Indexes are not supported
		We ARE type strict, unlike MySQL (even MySQL strict mode won't complain if a type conversion is always lossless, such as integer to string)
		We only really support expressions in certain places in a query
		Data Control Language (DCL) is not supported
		Semi-colons to split queries are not supported at the driver level
		Temporary tables are not supported
		Views are not supported
		Integrity checking (field constraints, CHECK) is not supported
		Transactions are not supported
		Full-text-search is not supported
		Special foreign key support is not supported
		INTERSECT and EXCEPT are not supported
		JOIN's are not supported in DELETE or UPDATE queries
		Sub-query support is limited to the IN and EXISTS constructs
		Character set support is just whatever ocPortal is set to; there is no special supported
		SELECT INTO is not supported
		LIMIT's on UPDATE queries not supported
		Expressions in ORDER BY clauses will be ignored
		Default values for fields are not supported
		Field naming for things like COUNT(*) will not be consistent with MySQL
		You must specify the field names in INSERT queries
		Expressions are not supported in the SELECT clause, except inside aggregate functions
	This database system is intended only for ocPortal, and not as a general purpose database. In ocPortal our philosophy is to write logic in PHP, not SQL, hence the subset supported.
	Also as we have to target MySQL-4.0 we can't implement some more sophisticated featured, in case programmers rely on them!
*/

/**
 * Standard code module initialisation function.
 */
function init__database__xml()
{
	global $SCHEMA_CACHE,$DIR_CONTENTS_CACHE;
	$SCHEMA_CACHE=array();
	$DIR_CONTENTS_CACHE=array();

	global $DELIMITERS_FLIPPED,$DELIMITERS,$SYMBOL_DELIMINITER,$DELIMITERS_ALPHA;
	$DELIMITERS=array_merge(array("\t",' ',"\n"),_get_sql_keywords());
	sort($DELIMITERS);
	$DELIMITERS_FLIPPED=array_flip($DELIMITERS);
	$SYMBOL_DELIMINITER=array_flip(array("\t",' ',"\n",'+','-','*','/','>','<','=',"'",'"',"\\'",'(',')',','));
	foreach ($DELIMITERS as $d)
	{
		if (!isset($DELIMITERS_ALPHA[$d[0]])) $DELIMITERS_ALPHA[$d[0]]=array();
		$DELIMITERS_ALPHA[$d[0]][]=$d;
	}

	global $TABLE_BASES;
	$TABLE_BASES=array();

	// Support for chaining a DB- to make reads faster
	global $SITE_INFO;
	if ((array_key_exists('db_chain_type',$SITE_INFO)) && (!running_script('xml_db_import')) && (get_param_integer('keep_no_chain',0)!=1))
	{
		require_code('database/'.$SITE_INFO['db_chain_type']);
		$GLOBALS['XML_CHAIN_DB']=new database_driver($SITE_INFO['db_chain'],$SITE_INFO['db_chain_host'],$SITE_INFO['db_chain_user'],$SITE_INFO['db_chain_password'],get_table_prefix(),false,object_factory('Database_Static_'.$SITE_INFO['db_chain_type']));
	} else
	{
		$GLOBALS['XML_CHAIN_DB']=NULL;
	}
}

/**
 * Get a list of all SQL keywords
 *
 * @return array			List of keywords
 */
function _get_sql_keywords()
{
	return array(
		'CONCAT','LENGTH','REPLACE',
		'WHERE',
		'SELECT','FROM','AS','UNION','ALL','DISTINCT',
		'INSERT','INTO','VALUES','UPDATE','SET','DELETE',
		'ALTER','CREATE','DROP','ADD','CHANGE','RENAME','DEFAULT','TABLE','PRIMARY','KEY',
		'LIKE','IF','NOT','IS','NULL','AND','OR','BETWEEN','IN','EXISTS',
		'GROUP','BY','ORDER','ASC','DESC',
		'JOIN','OUTER','INNER','LEFT','RIGHT','ON',
		'COUNT','SUM','AVG','COALESCE','MAX','MIN',
		'LIMIT',
		'+','-','*','/',
		'<>','>','<','>=','<=','=',
		'"',"'","\\'",
		'(',')',',',
		// Anything else is put into a value token
		// Tokens are delimited by white space or one of the symbol tokens
	);
}

class Database_Static_xml
{

	/**
	 * Find whether the database may run GROUP BY unfettered with restrictions on the SELECT'd fields having to be represented in it or aggregate functions
	 *
	 * @return boolean		Whether it can
	 */
	function can_arbitrary_groupby()
	{
		return true;
	}

	/**
	 * Get the default user for making db connections (used by the installer as a default).
	 *
	 * @return string			The default user for db connections
	 */
	function db_default_user()
	{
		return '';
	}

	/**
	 * Get the default password for making db connections (used by the installer as a default).
	 *
	 * @return string			The default password for db connections
	 */
	function db_default_password()
	{
		return '';
	}

	/**
	 * Find whether subquery support is present
	 *
	 * @param  array			A DB connection
	 * @return boolean		Whether it is
	 */
	function db_has_subqueries($db)
	{
		return true;
	}

	/**
	 * Get a map of ocPortal field types, to actual mySQL types.
	 *
	 * @return array			The map
	 */
	function db_get_type_remap()
	{
		$type_remap=array(
							'AUTO'=>'AUTO',
							'AUTO_LINK'=>'AUTO_LINK',
							'INTEGER'=>'INTEGER',
							'UINTEGER'=>'UINTEGER',
							'SHORT_INTEGER'=>'SHORT_INTEGER',
							'REAL'=>'REAL',
							'BINARY'=>'BINARY',
							'USER'=>'USER',
							'GROUP'=>'GROUP',
							'TIME'=>'TIME',
							'LONG_TRANS'=>'LONG_TRANS',
							'SHORT_TRANS'=>'SHORT_TRANS',
							'SHORT_TEXT'=>'SHORT_TEXT',
							'LONG_TEXT'=>'LONG_TEXT',
							'ID_TEXT'=>'ID_TEXT',
							'MINIID_TEXT'=>'MINIID_TEXT',
							'IP'=>'IP',
							'LANGUAGE_NAME'=>'LANGUAGE_NAME',
							'URLPATH'=>'URLPATH',
							'MD5'=>'MD5'
		);
		return $type_remap;
	}

	/**
	 * Create a table index.
	 *
	 * @param  ID_TEXT		The name of the table to create the index on
	 * @param  ID_TEXT		The index name (not really important at all)
	 * @param  string			Part of the SQL query: a comma-separated list of fields to use on the index
	 * @param  array			The DB connection to make on
	 */
	function db_create_index($table_name,$index_name,$_fields,$db)
	{
		// Indexes not supported
	}

	/**
	 * Change the primary key of a table.
	 *
	 * @param  ID_TEXT		The name of the table to create the index on
	 * @param  array			A list of fields to put in the new key
	 * @param  array			The DB connection to make on
	 */
	function db_change_primary_key($table_name,$new_key,$db)
	{
		$this->db_query('UPDATE db_meta SET m_type=REPLACE(m_type,\'*\',\'\') WHERE '.db_string_equal_to('m_table',$table_name),$db);
		foreach ($new_key as $_new_key)
		{
			$this->db_query('UPDATE db_meta SET m_type=CONCAT(\'*\',m_type) WHERE '.db_string_equal_to('m_table',$table_name).' AND '.db_string_equal_to('m_name',$_new_key),$db);
		}
	}

	/**
	 * Get the ID of the first row in an auto-increment table (used whenever we need to reference the first).
	 *
	 * @return integer			First ID used
	 */
	function db_get_first_id()
	{
		return 1;
	}

	/**
	 * Create a new table.
	 *
	 * @param  ID_TEXT		The table name
	 * @param  array			A map of field names to ocPortal field types (with *#? encodings)
	 * @param  array			The DB connection to make on
	 * @param  boolean		Whether to only do it if it does not currently exist
	 */
	function db_create_table($table_name,$fields,$db,$if_not_exists=false)
	{
		if (!is_null($GLOBALS['XML_CHAIN_DB']))
		{
			// DB chaining: It's a write query, so needs doing on chained DB too
			$GLOBALS['XML_CHAIN_DB']->static_ob->db_create_table($table_name,$fields,$GLOBALS['XML_CHAIN_DB']->connection_write,$if_not_exists);
		}

		$path=$db[0].'/'.$table_name;

		if (($if_not_exists) && (!file_exists($path))) return;

		$found_key=false;
		foreach ($fields as $type)
		{
			if (strpos($type,'*')!==false) $found_key=true;
		}
		if (!$found_key)
			fatal_exit('No key specified for table '.$table_name);

		@mkdir($path,0777);
		require_code('files');
		fix_permissions($path,0777);
		sync_file($path);
	}

	/**
	 * Encode an SQL statement fragment for a conditional to see if two strings are equal.
	 *
	 * @param  ID_TEXT		The attribute
	 * @param  string			The comparison
	 * @return string			The SQL
	 */
	function db_string_equal_to($attribute,$compare)
	{
		return $attribute."='".$this->db_escape_string($compare)."'";
	}

	/**
	 * Encode an SQL statement fragment for a conditional to see if two strings are not equal.
	 *
	 * @param  ID_TEXT		The attribute
	 * @param  string			The comparison
	 * @return string			The SQL
	 */
	function db_string_not_equal_to($attribute,$compare)
	{
		return $attribute."<>'".$this->db_escape_string($compare)."'";
	}

	/**
	 * This function is internal to the database system, allowing SQL statements to be build up appropriately. Some databases require IS NULL to be used to check for blank strings.
	 *
	 * @return boolean			Whether a blank string IS NULL
	 */
	function db_empty_is_null()
	{
		return false;
	}

	/**
	 * Delete a table.
	 *
	 * @param  ID_TEXT		The table name
	 * @param  array			The DB connection to delete on
	 */
	function db_drop_if_exists($table_name,$db)
	{
		if (!is_null($GLOBALS['XML_CHAIN_DB']))
		{
			// DB chaining: It's a write query, so needs doing on chained DB too
			$GLOBALS['XML_CHAIN_DB']->static_ob->db_drop_if_exists($table_name,$GLOBALS['XML_CHAIN_DB']->connection_write);
		}

		$file_path=$db[0].'/'.$table_name;
		$dh=@opendir($file_path);
		if ($dh!==false)
		{
			while (($file=readdir($dh))!==false)
			{
				if ((substr($file,-4)=='.xml') || (substr($file,-13)=='.xml-volatile'))
				{
					unlink($file_path.'/'.$file);
					sync_file($file_path.'/'.$file);
				}
			}
			closedir($dh);
			@rmdir($file_path);
			sync_file($file_path);
		}

		global $SCHEMA_CACHE,$DIR_CONTENTS_CACHE;
		unset($SCHEMA_CACHE[$table_name]);
		unset($DIR_CONTENTS_CACHE[$table_name]);
	}

	/**
	 * Determine whether the database is a flat file database, and thus not have a meaningful connect username and password.
	 *
	 * @return boolean			Whether the database is a flat file database
	 */
	function db_is_flat_file_simple()
	{
		return true;
	}

	/**
	 * Encode a LIKE string comparision fragement for the database system. The pattern is a mixture of characters and ? and % wilcard symbols.
	 *
	 * @param  string			The pattern
	 * @return string			The encoded pattern
	 */
	function db_encode_like($pattern)
	{
		return $this->db_escape_string($pattern);
	}

	/**
	 * Close the database connections. We don't really need to close them (will close at exit), just disassociate so we can refresh them.
	 */
	function db_close_connections()
	{
	}

	/**
	 * Get a database connection. This function shouldn't be used by you, as a connection to the database is established automatically.
	 *
	 * @param  boolean		Whether to create a persistant connection
	 * @param  string			The database name
	 * @param  string			The database host (the server)
	 * @param  string			The database connection username
	 * @param  string			The database connection password
	 * @param  boolean		Whether to on error echo an error and return with a NULL, rather than giving a critical error
	 * @return ?array			A database connection (NULL: failed)
	 */
	function db_get_connection($persistent,$db_name,$db_host,$db_user,$db_password,$fail_ok=false)
	{
		unset($persistent); // Un-used
		unset($db_host); // Un-used
		unset($db_user); // Un-used
		unset($db_password); // Un-used
		unset($fail_ok); // Un-used

		if ((strpos($db_name,'\\')===false) && (strpos($db_name,'/')===false))
			$db_name=get_custom_file_base().'/uploads/website_specific/'.$db_name;
		if (!file_exists($db_name)) // Will create on first usage
		{
			mkdir($db_name,0777);
			require_code('files');
			fix_permissions($db_name,0777);
			sync_file($db_name);
		}

		return array($db_name);
	}

	/**
	 * Find whether full-text-search is present
	 *
	 * @param  array			A DB connection
	 * @return boolean		Whether it is
	 */
	function db_has_full_text($db)
	{
		return is_null($GLOBALS['XML_CHAIN_DB'])?false:$GLOBALS['XML_CHAIN_DB']->static_ob->db_has_full_text($GLOBALS['XML_CHAIN_DB']->connection_read);
	}

	/**
	 * Assemble part of a WHERE clause for doing full-text search
	 *
	 * @param  string			Our match string
	 * @param  boolean		Whether to do a boolean full text search
	 * @return string			Part of a WHERE clause for doing full-text search
	 */
	function db_full_text_assemble($content,$boolean)
	{
		return is_null($GLOBALS['XML_CHAIN_DB'])?'':$GLOBALS['XML_CHAIN_DB']->static_ob->db_full_text_assemble($content,$boolean);
	}

	/**
	 * Find whether full-text-boolean-search is present
	 *
	 * @return boolean		Whether it is
	 */
	function db_has_full_text_boolean()
	{
		return is_null($GLOBALS['XML_CHAIN_DB'])?false:$GLOBALS['XML_CHAIN_DB']->static_ob->db_has_full_text_boolean($GLOBALS['XML_CHAIN_DB']->connection_read);
	}

	/**
	 * Escape a string so it may be inserted into a query. If SQL statements are being built up and passed using db_query then it is essential that this is used for security reasons. Otherwise, the abstraction layer deals with the situation.
	 *
	 * @param  string			The string
	 * @return string			The escaped string
	 */
	function db_escape_string($string)
	{
		return addslashes($string);
	}

	/**
	 * This function is a very basic query executor. It shouldn't usually be used by you, as there are abstracted versions available.
	 *
	 * @param  string			The complete SQL query
	 * @param  array			A DB connection
	 * @param  ?integer		The maximum number of rows to affect (NULL: no limit)
	 * @param  ?integer		The start row to affect (NULL: no specification)
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @param  boolean		Whether to get the autoincrement ID created for an insert query
	 * @param  boolean		Whether to force the query to execute on the XML database driver (won't optimise by using MySQL). Useful for calls happening for multi-part queries from within this DB driver
	 * @param  boolean		Whether we are saving as a 'volatile' file extension
	 * @return ?mixed			The results (NULL: no results), or the insert ID
	 */
	function db_query($query,$db,$max=NULL,$start=NULL,$fail_ok=false,$get_insert_id=false,$no_syndicate=false,$save_as_volatile=false)
	{
		global $DELIMITERS_FLIPPED,$DELIMITERS,$SYMBOL_DELIMINITER;

		// LEXING STAGE
		// ------------

		$i=0;
		$query.=' '; // Cheat so that we do not have to handle the end state differently
		$len=strlen($query);
		$tokens=array();
		$current_token='';
		$doing_symbol_delimiter=true;
		while ($i<$len)
		{
			$next=$query[$i];

			if (($next=="'") || ($next=='"'))
			{
				if (trim($current_token)!='')
				{
					if (isset($DELIMITERS_FLIPPED[strtoupper($current_token)]))
					{
						$tokens[]=strtoupper($current_token);
					} else
					{
						$tokens[]=$current_token;
					}
				}
				$current_token='';

				$i++;
				while ($i<$len)
				{
					$next=$query[$i];

					if ($next=='\\')
					{
						$i++;
						$next=$query[$i];
						$current_token.=$next;
					} else
					{
						if (($next=="'") || ($next=='"'))
						{
							$tokens[]="'";
							$tokens[]=$current_token;
							$tokens[]="'";
							break;
						} else
						{
							$current_token.=$next;
						}
					}

					$i++;
				}
				$current_token='';
				$doing_symbol_delimiter=true;
			} else
			{
				$symbol_delimiter_coming=((isset($SYMBOL_DELIMINITER[$next])) && ((isset($DELIMITERS_FLIPPED[$next])) || (($i+1<$len) && (isset($DELIMITERS_FLIPPED[$next.$query[$i+1]]))))); //  (NB: symbol delimiters are a maximum of two in length)
				if ( /*When token ends, which is..*/
					($symbol_delimiter_coming || $doing_symbol_delimiter) /*Case of toggling from symbol to text or vice-versa and we find a delimitor is coming. When symbol delimiter arrives or we are doing a symbol deliminator */
					&&
					(!$this->is_start_of_delimiter($current_token.$next)) /*And the next character does not fit onto the end of our current token*/
				)
				{
					if (trim($current_token)!='')
					{
						if (isset($DELIMITERS_FLIPPED[strtoupper($current_token)]))
						{
							$tokens[]=strtoupper($current_token);
						} else
						{
							$tokens[]=$current_token;
						}
					}
					$current_token=$next;
					$doing_symbol_delimiter=(isset($SYMBOL_DELIMINITER[$next]));
				} else
				{
					$current_token.=$next;
					if ($doing_symbol_delimiter) $doing_symbol_delimiter=isset($SYMBOL_DELIMINITER[$next]);
				}
			}

			$i++;
		}

		$query=substr($query,0,$len-1);

		// PARSING/EXECUTION STAGE
		// -----------------------

		$random_key=mt_rand(0,min(2147483647,mt_getrandmax())); // Generated later, passed by reference. We will assume we only need one; multi inserts will need to each specify the key in full

		if ((!is_null($GLOBALS['XML_CHAIN_DB'])) && (!$no_syndicate))
		{
			if (substr(strtoupper($query),0,7)=='SELECT ')
			{
				$chain_connection=&$GLOBALS['XML_CHAIN_DB']->connection_read;
			} else
			{
				$chain_connection=&$GLOBALS['XML_CHAIN_DB']->connection_write;
			}
			if (count($chain_connection)>4) // Okay, we can't be lazy anymore
			{
				$chain_connection=call_user_func_array(array($GLOBALS['XML_CHAIN_DB']->static_ob,'db_get_connection'),$chain_connection);
				_general_db_init();
			}

			switch ($tokens[0])
			{
				case 'INSERT':
					// DB chaining: It's a write query, so needs doing on chained DB too
					//  But because it's an insert we may need to put in an auto-increment also
					$_inserts=$this->_do_query_insert__parse($tokens,$query,$db,$fail_ok);
					if (is_null($_inserts)) return NULL;
					list($table_name,$inserts)=$_inserts;
					$insert_keys=array_keys($inserts[0]);
					$query_new='INSERT INTO '.$table_name.' (';
					$schema=$this->_read_schema($db,$table_name,$fail_ok);
					global $TABLE_BASES;
					foreach ($schema as $key=>$val)
					{
						if ((preg_replace('#[^\w]#','',$val)=='AUTO') && (!in_array($key,$insert_keys)))
						{
							$insert_keys[]=$key;
							foreach (array_keys($inserts) as $i)
							{
								if ($i!=0) $random_key=mt_rand(0,min(2147483647,mt_getrandmax()));

								$inserts[$i][$key]=isset($TABLE_BASES[$table_name])?$TABLE_BASES[$table_name]:$this->db_get_first_id(); // We always want first record as '1', because we often reference it in a hard-coded way
								while ((file_exists($db[0].'/'.$table_name.'/'.strval($inserts[$i][$key]).'.xml')) || (file_exists($db[0].'/'.$table_name.'/'.$this->_guid($schema,$inserts[$i]).'.xml')) || (file_exists($db[0].'/'.$table_name.'/'.strval($inserts[$i][$key]).'.xml-volatile')) || (file_exists($db[0].'/'.$table_name.'/'.$this->_guid($schema,$inserts[$i]).'.xml-volatile')))
								{
									if ($GLOBALS['IN_MINIKERNEL_VERSION']==1) // In particular the f_groups/f_categories/calendar_types usage of tables references ID numbers for things. But let's just make all installer stuff linear
									{
										$inserts[$i][$key]++;
										$TABLE_BASES[$table_name]=$inserts[$i][$key]+1;
									} else
									{
										if ($i!=0) $random_key=mt_rand(0,min(2147483647,mt_getrandmax()));
										$inserts[$i][$key]=$random_key; // We don't use auto-increment, we use randomisation. As otherwise when people sync over revision control there'd be conflicts
									}
								}
							}
						}
					}
					foreach ($insert_keys as $i=>$key)
					{
						if ($i!=0) $query_new.=',';
						$query_new.=$key;
					}
					$query_new.=')';
					foreach ($inserts as $ii=>$insert)
					{
						if ($ii!=0) $query_new.=', ('; else $query_new.=' VALUES (';
						$i=0;
						foreach ($insert as $value)
						{
							if ($i!=0) $query_new.=',';
							if (is_integer($value))
							{
								$query_new.=strval($value);
							}
							elseif (is_float($value))
							{
								$query_new.=float_to_raw_string($value);
							}
							elseif (is_null($value))
							{
								$query_new.='NULL';
							} else
							{
								$query_new.='\''.db_escape_string($value).'\'';
							}
							$i++;
						}
						$query_new.=')';
					}

					$GLOBALS['XML_CHAIN_DB']->static_ob->db_query($query_new,$chain_connection,$max,$start,$fail_ok,$get_insert_id);
					break;

				case 'UPDATE':
				case 'DELETE':
					// DB chaining: It's a write query, so needs doing on chained DB too
					$GLOBALS['XML_CHAIN_DB']->static_ob->db_query($query,$chain_connection,$max,$start,$fail_ok,$get_insert_id);
					break;

				case 'SELECT':
					return $GLOBALS['XML_CHAIN_DB']->static_ob->db_query($query,$chain_connection,$max,$start,$fail_ok,$get_insert_id);
			}
		}

		switch ($tokens[0])
		{
			case 'ALTER':
				return $this->_do_query_alter($tokens,$query,$db,$fail_ok);

			case 'CREATE':
				return $this->_do_query_create($tokens,$query,$db,$fail_ok);

			case 'INSERT':
				return $this->_do_query_insert($tokens,$query,$db,$fail_ok,$get_insert_id,$random_key,$save_as_volatile);

			case 'UPDATE':
				return $this->_do_query_update($tokens,$query,$db,$max,$start,$fail_ok);

			case 'DELETE':
				return $this->_do_query_delete($tokens,$query,$db,$max,$start,$fail_ok);

			case 'SELECT':
				$at=0;
				$results=$this->_do_query_select($tokens,$query,$db,$max,$start,$fail_ok,$at);
				return $results;

			case 'DROP':
				return $this->_do_query_drop($tokens,$query,$db,$fail_ok);
		}

		return $this->_bad_query($query,$fail_ok,'Unrecognised query type, '.$tokens[0]);
	}

	/**
	 * See if an item is a prefix to something in the delimiter array.
	 *
	 * @param  string			The item
	 * @return boolean		Whether it is
	 */
	function is_start_of_delimiter($looking)
	{
		global $DELIMITERS_FLIPPED,$DELIMITERS,$DELIMITERS_ALPHA;

		$len=strlen($looking);
		$looking=strtoupper($looking);
		if ($len==1) return isset($DELIMITERS_FLIPPED[$looking]);
		if (isset($DELIMITERS_ALPHA[$looking[0]]))
		{
			foreach ($DELIMITERS_ALPHA[$looking[0]] as $d)
			{
				if (substr($d,0,$len)==$looking) return true;
			}
		}
		return false;
	}

	/**
	 * Find the schema for a table.
	 *
	 * @param  array			The database
	 * @param  string			The table name
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return ?array			The schema map (NULL: not found)
	 */
	function _read_schema($db,$table_name,$fail_ok=false)
	{
		global $SCHEMA_CACHE;
		if (array_key_exists($table_name,$SCHEMA_CACHE)) return $SCHEMA_CACHE[$table_name];

		$table_prefix=get_table_prefix();

		$schema_query='SELECT m_name,m_type FROM '.$table_prefix.'db_meta WHERE '.db_string_equal_to('m_table',substr($table_name,strlen($table_prefix)));

		if ($table_name==$table_prefix.'db_meta')
		{
			$fields=array(
				array(
					'm_name'=>'m_table',
					'm_type'=>'*ID_TEXT',
				),
				array(
					'm_name'=>'m_name',
					'm_type'=>'*ID_TEXT',
				),
				array(
					'm_name'=>'m_type',
					'm_type'=>'ID_TEXT',
				),
			);
		}
		elseif ($table_name==$table_prefix.'db_meta_indices')
		{
			$fields=array(
				array(
					'm_name'=>'i_table',
					'm_type'=>'*ID_TEXT',
				),
				array(
					'm_name'=>'i_name',
					'm_type'=>'*ID_TEXT',
				),
				array(
					'm_name'=>'i_fields',
					'm_type'=>'*ID_TEXT',
				),
			);
		} else
		{
			if (get_db_type()!='xml')
			{
				$fields=$GLOBALS['SITE_DB']->query($schema_query);
			} else
			{
				$fields=$this->db_query($schema_query,$db,NULL,NULL,$fail_ok);
			}
			if (is_null($fields)) return array(); // Can happen during installation
		}

		$schema=array();
		foreach ($fields as $f)
		{
			$schema[$f['m_name']]=$f['m_type'];
		}

		if (count($schema)==0)
		{
			if (!$fail_ok)
			{
				fatal_exit('Internal error: missing schema for '.$table_name);
			} else
			{
				return NULL;
			}
		}

		$SCHEMA_CACHE[$table_name]=$schema;

		return $schema;
	}

	/**
	 * Type check some data destined to go into a table.
	 *
	 * @param  array			The schema
	 * @param  array			The data
	 * @param  string			Query that was executed
	 */
	function _type_check($schema,$record,$query)
	{
		foreach ($record as $key=>$val)
		{
			$schema_type=preg_replace('#[^\w]#','',$schema[$key]);

			/*if (in_array($schema_type,array('AUTO','AUTO_LINK','INTEGER','UINTEGER','SHORT_INTEGER','BINARY','USER','GROUP','TIME','SHORT_TRANS','LONG_TRANS')))
				$val=@intval($val);*/

			if (is_integer($val))
			{
				if (!in_array($schema_type,array('REAL','AUTO','AUTO_LINK','INTEGER','UINTEGER','SHORT_INTEGER','BINARY','USER','GROUP','TIME','SHORT_TRANS','LONG_TRANS')))
					$this->_bad_query($query,false,'Database type strictness error: '.$schema_type.' wanted for '.$key.' field, but integer was given');

				if (($val<0) && ($schema_type=='UINTEGER'))
					$this->_bad_query($query,false,'Database type strictness error: '.$schema_type.' wanted for '.$key.' field (negative number given)');

				if (($val!=0) && ($val!=1) && ($schema_type=='BINARY'))
					$this->_bad_query($query,false,'Database type strictness error: '.$schema_type.' wanted for '.$key.' field (number given was not 0 or 1)');
			}
			elseif (is_string($val))
			{
				$string_types=array(
					'SHORT_TEXT'=>255,
					'LONG_TEXT'=>NULL,
					'ID_TEXT'=>80,
					'MINIID_TEXT'=>40,
					'IP'=>40,
					'LANGUAGE_NAME'=>5,
					'URLPATH'=>255,
					'MD5'=>33,
					'UINTEGER'=>10, // Fudge as we need to send in unsigned integers using strings, as PHP can't hold them
				);

				if (!in_array($schema_type,array_keys($string_types)))
					$this->_bad_query($query,false,'Database type strictness error: '.$schema_type.' wanted for '.$key.' field, but string ('.$val.') was given');

				$max_length=$string_types[$schema_type];
				if ((!is_null($max_length)) && (strlen($val)>$max_length))
					$this->_bad_query($query,false,'Database type strictness error: '.$schema_type.' wanted for '.$key.' field (text too long, maximum is '.integer_format($max_length).')');
			}
			elseif (is_float($val))
			{
				if (!in_array($schema_type,array('REAL')))
					$this->_bad_query($query,false,'Database type strictness error: '.$schema_type.' wanted for '.$key.' field, but float was given');
			}
			elseif (is_null($val))
			{
				if (strpos($schema[$key],'?')===false)
					$this->_bad_query($query,false,'Database type strictness error: '.$schema_type.' wanted for '.$key.' field, but NULL was given');
			}
			else
			{
				$this->_bad_query($query,false,'Database type strictness error: '.$schema_type.' wanted for '.$key.' field, but '.gettype($val).' was given');
			}
		}
	}

	/**
	 * Read in all the records from a table.
	 *
	 * @param  array			Database connection
	 * @param  string			The table name
	 * @param  string			What the table will be renamed to (blank: N/A)
	 * @param  ?array			Schema to type-set against (NULL: do not do type-setting)
	 * @param  ?array			Expression filtering results (used for optimisation, seeing if we can get a quick key match) (NULL: no data to filter with)
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @param  string			Query that was executed
	 * @param  boolean		Whether to include fields that are present in the actual records but not in our schema
	 * @return ?array			The collected records (NULL: error)
	 */
	function _read_all_records($db,$table_name,$table_as,$schema,$where_expr,$fail_ok,$query,$include_unused_fields=false)
	{
		$records=array();
		$key_fragments=''; // We can do a filename substring search to stop us having to parse ALL
		$must_contain=NULL;
		if ((!is_null($schema)) && (!is_null($where_expr))) // Try for an efficient filename-based lookup
		{
			$keys=array();
			foreach ($schema as $key=>$type)
			{
				if (strpos($type,'*')!==false)
					$keys[]=$key;
			}

			foreach ($keys as $i=>$key)
			{
				if (strpos($key,'.')!==false)
				{
					if (substr($key,0,strpos($key,'.'))==$table_as)
					{
						$key=substr($key,strpos($key,'.')+1);
					} else
					{
						continue;
					}
				}
				$keys[$i]=$key;
			}
			$keys=array_unique($keys);

			$key_lookup=true;
			$where_expr_compressed=$this->_turn_where_expr_to_map($where_expr,$table_as,$schema);
			$where_expr_compressed_b=$this->_turn_where_expr_to_map($where_expr,$table_as,$schema,true);
			$must_contain=array();

			foreach ($where_expr_compressed_b as $key=>$val)
			{
				$new_val=mixed();
				if (is_string($val))
				{
					$new_val=$val;
				}
				elseif (is_integer($val))
				{
					$new_val=strval($val);
				}
				elseif (is_float($val))
				{
					$new_val=float_to_raw_string($val);
				}
				elseif (is_array($val))
				{
					$new_val=array();
					foreach ($val as $_val)
					{
						if (is_string($_val))
						{
							$new_val[]=$_val;
						}
						elseif (is_integer($_val))
						{
							$new_val[]=strval($_val);
						}
						elseif (is_float($_val))
						{
							$new_val[]=float_to_raw_string($_val);
						}
					}
				} else
				{
					$new_val='';
				}

				$must_contain[$key]=$new_val;
			}
			sort($keys);
			foreach ($keys as $key)
			{
				if ($key_fragments!='') $key_fragments.=',';
				if ($key!='id')
					$key_fragments.=str_replace('#','\#',preg_quote($key.'='));

				if (!in_array($key,array_keys($where_expr_compressed)))
				{
					$key_lookup=false;
					$key_fragments.='.*';
				} else
				{
					$val=$where_expr_compressed[$key];
					$new_val='';
					if (is_array($val))
					{
						if (count($val)==1)
						{
							$val=$val[0];
						} else
						{
							$key_lookup=false;
							$key_fragments.='(';
							foreach ($val as $i=>$possible)
							{
								if ($i!=0) $key_fragments.='|';

								if (is_string($possible))
								{
									$new_val=$possible;
								}
								elseif (is_integer($possible))
								{
									$new_val=strval($possible);
								}
								elseif (is_float($possible))
								{
									$new_val=float_to_raw_string($possible);
								}
								$key_fragments.=str_replace('#','\#',preg_quote($this->_escape_name($new_val)));
							}
							$key_fragments.=')';
							continue;
						}
					}
					if (is_string($val))
					{
						$new_val=$val;
					}
					elseif (is_integer($val))
					{
						$new_val=strval($val);
					}
					elseif (is_float($val))
					{
						$new_val=float_to_raw_string($val);
					}

					$key_fragments.=str_replace('#','\#',preg_quote($this->_escape_name($new_val)));
				}
			}
			$key_buildup=$this->_guid($schema,$where_expr_compressed);

			if (($key_lookup) && ($key_buildup!=''))
			{
				$file_exists_xml=file_exists($db[0].'/'.$table_name.'/'.$key_buildup.'.xml');
				$file_exists_xml_volatile=file_exists($db[0].'/'.$table_name.'/'.$key_buildup.'.xml-volatile');
				if (($file_exists_xml) || ($file_exists_xml_volatile))
				{
					$the_key=preg_replace('#\.[\w\-]+$#','',$key_buildup);
					$suffix=$file_exists_xml?'.xml':'.xml-volatile';
					$records[$the_key]=$this->_read_record($db[0].'/'.$table_name.'/'.$key_buildup.$suffix,$schema,NULL,$include_unused_fields);
					if ($table_name==get_table_prefix().'translate')
					{
						$sup_file=$db[0].'/'.$table_name.'/sup/'.$key_buildup.'.xml-volatile';
						if (file_exists($sup_file))
						{
							$sup_record=$this->_read_record($sup_file,$schema,NULL,$include_unused_fields);
							$records[$the_key]['text_parsed']=$sup_record['text_parsed'];
						}
					}
					return $records;
				} else
				{
					return array();
				}
			}
		}

		global $DIR_CONTENTS_CACHE;
		if (!isset($DIR_CONTENTS_CACHE[$table_name]))
		{
			if (!file_exists($db[0].'/'.$table_name))
			{
				mkdir($db[0].'/'.$table_name,0777);
				require_code('files');
				fix_permissions($db[0].'/'.$table_name,0777);
				sync_file($db[0].'/'.$table_name);
			}
			@chdir($db[0].'/'.$table_name);
			$dh=@glob('{,.}*.{xml,xml-volatile}',GLOB_NOSORT|GLOB_BRACE);
			if ($dh===false) $dh=array();
			@chdir(get_file_base());
			if (file_exists($db[0].'/'.$table_name.'/.xml'))
				$dh[]='.xml';
			elseif (file_exists($db[0].'/'.$table_name.'/.xml-volatile'))
				$dh[]='.xml-volatile';
			$DIR_CONTENTS_CACHE[$table_name]=$dh;
		} else
		{
			$dh=$DIR_CONTENTS_CACHE[$table_name];
		}
		if (($dh===false) && ($fail_ok)) return NULL;
		if ($dh===false) critical_error('PASSON','Failure to read table '.$table_name);
		$regexp='#^'.$key_fragments.'('.preg_quote('.xml').'|'.preg_quote('.xml-volatile').')$#';

		foreach ($dh as $file)
		{
			if ($key_fragments!='')
			{
				if (preg_match($regexp,$file)==0)
					continue;
			}
			$full_path=$db[0].'/'.$table_name.'/'.$file;
			if ((strlen($full_path)>=255) && (strpos(strtolower(PHP_OS),'win')!==false)) continue; // :(
			$read=$this->_read_record($full_path,$schema,$must_contain,$include_unused_fields);
			if (!is_null($read))
			{
				$the_key=preg_replace('#\.[\w\-]+$#','',$file);
				$records[$the_key]=$read;

				if ($table_name==get_table_prefix().'translate')
				{
					$sup_file=$db[0].'/'.$table_name.'/sup/'.preg_replace('#\.\w+$#','',$file).'.xml-volatile';
					if (file_exists($sup_file))
						$records[$the_key]+=$this->_read_record($sup_file,$schema,NULL,$include_unused_fields);
				}
			}
		}

		return $records;
	}

	/**
	 * Take an expression and do our best to collapse it into a fixed mapping of stuff we know we are going to AND.
	 *
	 * @param  array			The expression parse tree
	 * @param  string			What the table will be renamed to (blank: N/A)
	 * @param  ?array			Schema to type-set against (NULL: do not do type-setting)
	 * @param  boolean		Whether to do a not-full-accurate search
	 * @return array			AND map
	 */
	function _turn_where_expr_to_map($where_expr,$table_as,$schema=NULL,$not_full_accuracy=false)
	{
		if ($where_expr[0]=='BRACKETED')
		{
			return $this->_turn_where_expr_to_map($where_expr[1],$table_as,$schema,$not_full_accuracy);
		}
		if ($where_expr[0]=='AND')
		{
			return array_merge($this->_turn_where_expr_to_map($where_expr[1],$table_as,$schema,$not_full_accuracy),$this->_turn_where_expr_to_map($where_expr[2],$table_as,$schema,$not_full_accuracy));
		}
		if ($where_expr[0]=='OR')
		{
			$alpha=$this->_turn_where_expr_to_map($where_expr[1],$table_as,$schema,$not_full_accuracy);
			$beta=$this->_turn_where_expr_to_map($where_expr[2],$table_as,$schema,$not_full_accuracy);
			$_alpha=array_keys($alpha);
			$_beta=array_keys($beta);
			if ((count($alpha)==1) && (count($beta)==1) && ($_alpha==$_beta))
			{
				$alpha[$_alpha[0]]=array_merge(is_array($alpha[$_alpha[0]])?$alpha[$_alpha[0]]:array($alpha[$_alpha[0]]),is_array($beta[$_beta[0]])?$beta[$_beta[0]]:array($beta[$_beta[0]]));
				return $alpha;
			}
		}
		if (($where_expr[0]=='=') && ($where_expr[1][0]=='LITERAL') && ($where_expr[2][0]=='FIELD'))
		{
			$where_expr=array($where_expr[0],$where_expr[2],$where_expr[1]);
		}
		if (($where_expr[0]=='LIKE') && ($where_expr[1][0]=='FIELD') && ($where_expr[2][0]=='LITERAL') && ($not_full_accuracy))
		{
			$key=$where_expr[1][1];
			if ($table_as!='')
			{
				$key=preg_replace('#^'.str_replace('#','\#',preg_quote($table_as)).'\.#','',$key);
				if (strpos($key,'.')!==false) return array(); // Not for our table
			}

			if ((!is_null($schema)) && (!array_key_exists($key,$schema)))
				return array(); // Not in our table (join involved. must be in other join)

			if (substr($where_expr[2][1],0,1)=='%') $where_expr[2][1]=substr($where_expr[2][1],1);
			if (substr($where_expr[2][1],-1)=='%') $where_expr[2][1]=substr($where_expr[2][1],0,strlen($where_expr[2][1])-1);
			if ((strpos($where_expr[2][1],'%')!==false) || (strpos($where_expr[2][1],'?')!==false))
				return array();

			return array($key=>$where_expr[2][1]);
		}
		if (($where_expr[0]=='=') && ($where_expr[1][0]=='FIELD') && ($where_expr[2][0]=='LITERAL'))
		{
			$key=$where_expr[1][1];
			if ($table_as!='')
			{
				$key=preg_replace('#^'.str_replace('#','\#',preg_quote($table_as)).'\.#','',$key);
				if (strpos($key,'.')!==false) return array(); // Not for our table
			}
			if ((!is_null($schema)) && (!array_key_exists($key,$schema)))
				return array(); // Not in our table (join involved. must be in other join)
			return array($key=>$where_expr[2][1]);
		}
		return array();
	}

	/**
	 * Read a record from an XML file.
	 *
	 * @param  PATH			The file path
	 * @param  ?array			Schema to type-set against (NULL: do not do type-setting)
	 * @param  ?array			Substrings to check it is in, used for performance (NULL: none)
	 * @param  boolean		Whether to include fields that are present in the actual records but not in our schema
	 * @return ?array			The record map (NULL: does not contain requested substrings)
	 */
	function _read_record($path,$schema=NULL,$must_contain_strings=NULL,$include_unused_fields=false)
	{
		if (file_exists($path.'.mine')) $path.='.mine';
		$file_contents=@file_get_contents($path);
		if ($file_contents===false) warn_exit(do_lang_tempcode('INTERNAL_ERROR'));

		if (!is_null($must_contain_strings))
		{
			foreach ($must_contain_strings as $match)
			{
				if (is_array($match))
				{
					$found=0;
					$possible_matches=$match;
					foreach ($possible_matches as $match2)
					{
						if ($match=='') continue;

						if (strpos($file_contents,xmlentities($match2))!==false)
						{
							$found++;
							break;
						}
					}
					if ($found==0) return NULL;
				} else
				{
					if ($match=='') continue;

					if (strpos($file_contents,xmlentities($match))===false)
					{
						return NULL;
					}
				}
			}
		}

		/*$ob=new xml_file_parse($file_contents); Too slow
		if (!is_null($ob->error)) fatal_exit($ob->error);
		$_record=$ob->output;*/
		// This is much faster, even though it's a bit of a hack as it assumes all records are as ocPortal would write them
		$bits=preg_split('#</?([^>]*)>#',$file_contents,-1,PREG_SPLIT_DELIM_CAPTURE);
		$_record=array();
		$bc=count($bits)-2;
		$i=0;
		if (trim($bits[$i])=='') $i++; // Whitespace between tags
		if ((!isset($bits[$i])) || ($bits[$i]!='ocportal')) warn_exit('Unrecognised XML in '.$path);
		$i++; // Skip past "ocPortal"
		if (trim($bits[$i])=='') $i++; // Whitespace between tags
		while ($i<$bc)
		{
			$field=$bits[$i];
			$i++;
			$data=$bits[$i];
			$i++;
			$i++; // Skip past closing tag
			if (trim($bits[$i])=='') $i++; // Whitespace between tags
			$_record[$field]=html_entity_decode($data,ENT_QUOTES,get_charset());
		}

		// Even if we did serialize with type information (we don't) we would still need to do type checking, because when we do add_table_field/alter_table_field we can't assume it will alter all non-committed records on other people's systems
		if (is_null($schema))
		{
			return $_record;
		} else
		{
			$record=array();
			foreach ($_record as $key=>$val)
			{
				$new_val=mixed();
				if ((!array_key_exists($key,$schema)) && (!$include_unused_fields)) continue; // Been deleted
				$type=$schema[$key];
				$schema_type=preg_replace('#[^\w]#','',$type);

				if (in_array($schema_type,array('AUTO','AUTO_LINK','INTEGER','UINTEGER','SHORT_INTEGER','BINARY','USER','GROUP','TIME','SHORT_TRANS','LONG_TRANS')))
				{
					if (((is_null($val)) || ($val==='')) && (substr($type,0,1)=='?'))
					{
						$new_val=NULL;
					} else
					{
						$new_val=@intval($val);
					}
				}
				elseif (in_array($schema_type,array('REAL')))
				{
					if (((is_null($val)) || ($val==='')) && (substr($type,0,1)=='?'))
					{
						$new_val=NULL;
					} else
					{
						$new_val=@floatval($val);
					}
				} else
				{
					$new_val=$val;
				}

				$record[$key]=$new_val;

				unset($schema[$key]);
			}

			foreach ($schema as $key=>$type)
			{
				$schema_type=preg_replace('#[^\w]#','',$type);

				if (in_array($schema_type,array('AUTO','AUTO_LINK','INTEGER','UINTEGER','SHORT_INTEGER','BINARY','USER','GROUP','TIME','SHORT_TRANS','LONG_TRANS')))
				{
					if (substr($type,0,1)=='?')
					{
						$record[$key]=NULL;
					} else
					{
						$record[$key]=0;
					}
				}
				elseif (in_array($schema_type,array('REAL')))
				{
					if (substr($type,0,1)=='?')
					{
						$record[$key]=0.0;
					} else
					{
						$record[$key]=0.0;
					}
				} else
				{
					$record[$key]='';
				}
			}
		}

		return $record;
	}

	/**
	 * Write in all the records to a table.
	 *
	 * @param  array			Database connection
	 * @param  string			The table name
	 * @param  array			The list of record maps
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 */
	function _write_records($db,$table_name,$records,$fail_ok=false)
	{
		foreach ($records as $guid=>$record)
		{
			if (!is_string($guid)) $guid=strval($guid); // As PHP can use type for array keys
			$this->_write_record($db,$table_name,$guid,$record,$fail_ok);
		}
	}

	/**
	 * Write a record to an XML file.
	 *
	 * @param  array			Database connection
	 * @param  string			The table name
	 * @param  string			The GUID
	 * @param  array			The record map
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @param  boolean		Whether we are saving as a 'volatile' file extension
	 */
	function _write_record($db,$table_name,$guid,$record,$fail_ok=false,$save_as_volatile=false)
	{
		$suffix=$save_as_volatile?'.xml-volatile':'.xml';

		if (!file_exists($db[0].'/'.$table_name))
		{
			mkdir($db[0].'/'.$table_name,0777);
			require_code('files');
			fix_permissions($db[0].'/'.$table_name,0777);
			sync_file($db[0].'/'.$table_name);
		}

		$path=$db[0].'/'.$table_name.'/'.$guid.$suffix;
		if (file_exists($path.'.mine')) $path.='.mine';

		if ($table_name==get_table_prefix().'translate') // Special code to store volatile text_parsed attribute externally
		{
			$record_copy=$record;
			unset($record_copy['text_original']);
			unset($record_copy['source_user']);
			unset($record_copy['broken']);
			unset($record_copy['importance_level']);
			$record['text_parsed']='';
			$this->_write_record($db,$table_name.'/sup',$guid,$record_copy,$fail_ok,true);
		}

		if ((strlen($path)>255) && (strpos(strtolower(PHP_OS),'win')!==false))
		{
			attach_message('File path too long on Windows ('.$path.')','warn');
			return;
		}
		$myfile=fopen($path,'wb');
		if (!defined('PHALANGER'))
			while (!flock($myfile,LOCK_EX)) sleep(1);
		fwrite($myfile,"<ocportal>\n");
		$val=mixed();
		foreach ($record as $key=>$val)
		{
			if (is_integer($val)) $val=strval($val);
			if (is_float($val)) $val=float_to_raw_string($val);
			if (is_null($val)) $val='';
			fwrite($myfile,"\t<".$key.">".xmlentities($val)."</".$key.">\n");
		}
		fwrite($myfile,"</ocportal>\n");
		fclose($myfile);
		require_code('files');
		fix_permissions($path,0666);
		sync_file($path);

		/*if (file_exists($db[0].'/'.$table_name.'/.svn/prop-base/'))		If we want them in subversion as binary, but we probably don't as merging can often work
		{
			$tpath=$db[0].'/'.$table_name.'/.svn/prop-base/'.$guid.$suffix;
			$myfile=fopen($tpath,'wb');
			fwrite($myfile,'K 13\nsvn:mime-type\nV 24\napplication/octet-stream\nEND');
			fclose($myfile);
			fix_permissions($tpath,0666);
			sync_file($tpath);
		}*/

		unset($GLOBALS['DIR_CONTENTS_CACHE'][$table_name]);

		$schema=$this->_read_schema($db,preg_replace('#/sup$#','',$table_name),$fail_ok);
		if (!is_null($schema))
		{
			$new_guid=$this->_guid($schema,$record);
			$new_path=$db[0].'/'.$table_name.'/'.$new_guid.$suffix;
			if ($path!=$new_path)
			{
				rename($path,$new_path);
				/*if (substr($path,-5)=='.mine')
					unlink();  Yuck, messy, we will ignore this potential problem - people should not edit stuff that is conflicted */
			}
		}
	}

	/**
	 * Write a record to an XML file.
	 *
	 * @param  PATH			The file path
	 * @param  array			Database connection
	 */
	function _delete_record($path,$db)
	{
		/* This generally is a bad idea. Things can get deleted then re-made, and we don't even need it. This command works better:
		svn status | grep '\!.*\.xml' | awk '{print $2;}' | xargs svn rm
		$svn_command='svn remove "'.str_replace('"','\"',$path).'"';
		//@shell_exec($svn_command);	Can't do as it would not execute with the correct permissions
		$new=!file_exists($db[0].'/deletions.sh');
		$command_file=@fopen($db[0].'/deletions.sh','at');
		if ($command_file!==false)
		{
			if ($new)
				fwrite($command_file,'#!/bin/sh'.chr(10));
			fwrite($command_file,$svn_command.chr(10));
			fclose($command_file);
			require_code('files');
			fix_permissions($db[0].'/deletions.sh',0777);
			sync_file($db[0].'/deletions.sh');
		}*/

		if (file_exists($path))
		{
			$myfile=fopen($path,'wb');
			if (!defined('PHALANGER'))
				while (!@flock($myfile,LOCK_EX)) sleep(1);
			fclose($myfile);
			@unlink($path);
			sync_file($path);
		}

		if (file_exists($path.'.mine'))
		{
			$myfile=fopen($path.'.mine','wb');
			if (!defined('PHALANGER'))
				while (!@flock($myfile,LOCK_EX)) sleep(1);
			fclose($myfile);
			unlink($path.'.mine');
			sync_file($path.'.mine');
		}
	}

	/**
	 * Check to see if there is a key conflict problem.
	 *
	 * @param  array			Database connection
	 * @param  string			The table name
	 * @param  array			The schema
	 * @param  array			The record
	 * @param  string			Query that was executed
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @param  ?string		The GUID representing what we have now (so we don't think we're conflicting with ourself) (NULL: not yet added)
	 * @return boolean		Whether there was a conflict
	 */
	function _key_conflict_check($db,$table_name,$schema,$record,$query,$fail_ok,$existing_identity=NULL)
	{
		$where='';
		foreach ($schema as $key=>$type)
		{
			if (strpos($type,'*')===false) continue;

			if ($where!='') $where.=' AND ';

			$value=$record[$key];
			if (is_null($value)) $where.=$key.'IS NULL';
			else
			{
				if (is_float($value)) $where.=$key.'='.float_to_raw_string($value);
				elseif (is_integer($value)) $where.=$key.'='.strval($value);
				else $where.=$key.'=\''.db_escape_string($value).'\'';
			}
		}

		$test_results=$this->db_query('SELECT * FROM '.$table_name.' WHERE '.$where,$db,2,NULL,$fail_ok,false,true);
		if (count($test_results)==0) return false;
		if (count($test_results)>1) return true;
		if ((count($test_results)==1) && (is_null($existing_identity))) return true;
		$is_different=($this->_guid($schema,$test_results[0])!=$existing_identity);
		return $is_different;
	}

	/**
	 * Execute a DROP query.
	 *
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return ?mixed			The results (NULL: no results)
	 */
	function _do_query_drop($tokens,$query,$db,$fail_ok)
	{
		$at=0;
		if (!$this->_parsing_expects($at,$tokens,'DROP',$query)) return NULL;
		$type=$this->_parsing_read($at,$tokens,$query);
		if ($type=='INDEX')
		{
			$index_name=$this->_parsing_read($at,$tokens,$query);
			if (!$this->_parsing_expects($at,$tokens,'ON',$query)) return NULL;
			$table_name=$this->_parsing_read($at,$tokens,$query);

			// We don't actually do indexes, so do nothing
		} elseif ($type=='TABLE')
		{
			$table_name=$this->_parsing_read($at,$tokens,$query);
			$this->db_drop_if_exists($table_name,$db);
		} else
		{
			return $this->_bad_query($query,$fail_ok,'Unrecognised DROP type, '.$type);
		}

		if (!$this->_parsing_check_ended($at,$tokens,$query)) return NULL;

		return NULL;
	}

	/**
	 * Execute an ALTER query.
	 *
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return ?mixed			The results (NULL: no results)
	 */
	function _do_query_alter($tokens,$query,$db,$fail_ok)
	{
		global $SCHEMA_CACHE;

		$at=0;
		if (!$this->_parsing_expects($at,$tokens,'ALTER',$query)) return NULL;
		if (!$this->_parsing_expects($at,$tokens,'TABLE',$query)) return NULL;
		$table_name=$this->_parsing_read($at,$tokens,$query);
		$op=$this->_parsing_read($at,$tokens,$query);
		switch ($op)
		{
			case 'RENAME':
				$new_table_name=$this->_parsing_read($at,$tokens,$query);
				rename($db[0].'/'.$table_name,$db[0].'/'.$new_table_name);
				unset($GLOBALS['DIR_CONTENTS_CACHE'][$table_name]);
				break;
			case 'CHANGE':
			case 'ADD':
				// Parse
				$column_name=$this->_parsing_read($at,$tokens,$query);
				if ($op=='CHANGE')
					$new_column_name=$this->_parsing_read($at,$tokens,$query);
				else
					$new_column_name=$column_name;
				$data_type=$this->_parsing_read($at,$tokens,$query);
				$next=$this->_parsing_read($at,$tokens,$query,true);
				if ($next=='DEFAULT')
				{
					$_default=$this->_parsing_read_expression($at,$tokens,$query,$db,false,false,$fail_ok);
					$default=$this->_execute_expression($_default,array(),$query);
				} else
				{
					$default=false;

					if (!is_null($next)) $at--;
				}
				$next=$this->_parsing_read($at,$tokens,$query,true);
				$allow_null=NULL; // No change
				if (!is_null($next))
				{
					if ($next=='NOT')
					{
						if (!$this->_parsing_expects($at,$tokens,'NULL',$query)) return NULL;
					}
					elseif ($next=='NULL')
					{
						$allow_null=true;
					}
					else
					{
						$at--;
					}
				}

				if ($default===false)
				{
					if ($allow_null)
					{
						$default=NULL;
					} else
					{
						if (in_array($data_type,array('AUTO','AUTO_LINK','INTEGER','UINTEGER','SHORT_INTEGER','BINARY','USER','GROUP','TIME','SHORT_TRANS','LONG_TRANS')))
						{
							return $this->_bad_query($query,false,'No DEFAULT given and NULL not allowed');
							//$default=0;
						}
						elseif (in_array($data_type,array('REAL')))
						{
							return $this->_bad_query($query,false,'No DEFAULT given and NULL not allowed');
							//$default=0.0;
						} else
						{
							$default='';
						}
					}
				}

				// Execute
				if ($op=='ADD')
				{
					$records=$this->_read_all_records($db,$table_name,'',NULL,NULL,$fail_ok,$query);
					if (is_null($records)) return NULL;
					foreach (array_keys($records) as $guid)
					{
						$records[$guid][$column_name]=$default;
					}
					$this->_write_records($db,$table_name,$records,$fail_ok);

					if (array_key_exists($table_name,$SCHEMA_CACHE))
						$SCHEMA_CACHE[$table_name][$column_name]=$data_type;
				}
				elseif ($op=='CHANGE')
				{
					// Actually we type-convert in real-time so no change actually needed. ocPortal would have updated the meta stuff separately
					if (array_key_exists($table_name,$SCHEMA_CACHE))
					{
						unset($SCHEMA_CACHE[$table_name][$column_name]);
						$SCHEMA_CACHE[$table_name][$new_column_name]=$data_type;
					}

					if ($new_column_name!=$column_name)
					{
						$records=$this->_read_all_records($db,$table_name,'',NULL,NULL,$fail_ok,$query,true);
						if (is_null($records)) return NULL;
						foreach (array_keys($records) as $guid)
						{
							$records[$guid][$new_column_name]=$records[$guid][$column_name];
							unset($records[$guid][$column_name]);
						}
						$this->_write_records($db,$table_name,$records,$fail_ok);
					}
				}

				if (!$this->_parsing_check_ended($at,$tokens,$query)) return NULL;

				return NULL;

			case 'DROP':
				// Parse
				if (!$this->_parsing_expects($at,$tokens,'COLUMN',$query)) return NULL;
				$column_name=$this->_parsing_read($at,$tokens,$query);

				// Execute
				$records=$this->_read_all_records($db,$table_name,'',NULL,NULL,$fail_ok,$query);
				if (is_null($records)) return NULL;
				foreach (array_keys($records) as $guid)
				{
					unset($records[$guid][$column_name]);
				}
				$this->_write_records($db,$table_name,$records,$fail_ok);

				unset($SCHEMA_CACHE[$table_name][$column_name]);

				if (!$this->_parsing_check_ended($at,$tokens,$query)) return NULL;

				return NULL;
		}

		return $this->_bad_query($query,false,'Expected ALTER TABLE ADD or ALTER TABLE DROP or ALTER TABLE CHANGE or ALTER TABLE RENAME');
	}

	/**
	 * Execute a CREATE query.
	 *
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return ?mixed			The results (NULL: no results)
	 */
	function _do_query_create($tokens,$query,$db,$fail_ok)
	{
		$at=0;
		if (!$this->_parsing_expects($at,$tokens,'CREATE',$query)) return NULL;
		if (!$this->_parsing_expects($at,$tokens,'TABLE',$query)) return NULL;
		$table_name=$this->_parsing_read($at,$tokens,$query);
		$if_not_exists=false;
		if ($table_name=='IF')
		{
			if (!$this->_parsing_expects($at,$tokens,'NOT',$query)) return NULL;
			if (!$this->_parsing_expects($at,$tokens,'EXISTS',$query)) return NULL;
			$table_name=$this->_parsing_read($at,$tokens,$query);
			$if_not_exists=true;
		}

		if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
		$fields=array();
		do
		{
			$column_name=$this->_parsing_read($at,$tokens,$query);
			if ($column_name=='PRIMARY')
			{
				if (!$this->_parsing_expects($at,$tokens,'KEY',$query)) return NULL;
				if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
				do
				{
					$token=$this->_parsing_read($at,$tokens,$query);
					$fields[$token]='*'.$fields[$token];
					$token=$this->_parsing_read($at,$tokens,$query);
				}
				while ($token==',');
				$at--;
				if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
			} else
			{
				$null=true;
				$type=$this->_parsing_read($at,$tokens,$query);

				$token=$this->_parsing_read($at,$tokens,$query);
				if ($token=='NOT')
				{
					if (!$this->_parsing_expects($at,$tokens,'NULL',$query)) return NULL;
					$null=false;
				}
				elseif ($token=='PRIMARY')
				{
					if (!$this->_parsing_expects($at,$tokens,'KEY',$query)) return NULL;
					$type='*'.$type;
				}
				else $at--;

				$fields[$column_name]=($null?'?':'').$type;
			}

			$next=$this->_parsing_read($at,$tokens,$query);
		}
		while ($next==',');
		$at--;
		if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;

		$this->db_create_table($table_name,$fields,$db,$if_not_exists);

		if (!$this->_parsing_check_ended($at,$tokens,$query)) return NULL;
	}

	/**
	 * Wrapper to execute an INSERT query.
	 *
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @param  boolean		Whether to get the autoincrement ID created for an insert query
	 * @param  ?integer		The random key that we would use (NULL: not generated yet)
	 * @param  boolean		Whether we are saving as a 'volatile' file extension
	 * @return ?mixed			The insert ID (NULL: not requested / error)
	 */
	function _do_query_insert($tokens,$query,$db,$fail_ok,$get_insert_id,&$random_key,$save_as_volatile=false)
	{
		$_inserts=$this->_do_query_insert__parse($tokens,$query,$db,$fail_ok);
		if (is_null($_inserts)) return NULL;
		list($table_name,$inserts)=$_inserts;
		return $this->_do_query_insert__execute($inserts,$table_name,$query,$db,$fail_ok,$get_insert_id,$random_key,$save_as_volatile);
	}

	/**
	 * Parse an INSERT query.
	 *
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return ?array			A pair: the table, and the rows to insert (NULL: error)
	 */
	function _do_query_insert__parse($tokens,$query,$db,$fail_ok)
	{
		// Parse
		$at=0;
		if (!$this->_parsing_expects($at,$tokens,'INSERT',$query)) return NULL;
		if (!$this->_parsing_expects($at,$tokens,'INTO',$query)) return NULL;
		$table_name=$this->_parsing_read($at,$tokens,$query);
		if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
		$record_basic=array();
		$reverse_index=array();
		do
		{
			$token=$this->_parsing_read($at,$tokens,$query);
			$record_basic[$token]=NULL;
			$reverse_index[]=$token;
			$token=$this->_parsing_read($at,$tokens,$query);
		}
		while ($token==',');
		$at--;
		if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
		if (!$this->_parsing_expects($at,$tokens,'VALUES',$query)) return NULL;
		$inserts=array();
		do
		{
			if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
			$record=$record_basic;
			$i=0;
			do
			{
				$expr=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
				$result=$this->_execute_expression($expr,array(),$query);
				$record[$reverse_index[$i]]=$result;
				$i++;
				$token=$this->_parsing_read($at,$tokens,$query);
			}
			while ($token==',');
			$at--;
			if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;

			// Store in our list
			$inserts[]=$record;

			// Continue
			$token=$this->_parsing_read($at,$tokens,$query,true);
		}
		while ($token===',');
		if (!is_null($token)) $at--;

		if (!$this->_parsing_check_ended($at,$tokens,$query)) return NULL;

		return array($table_name,$inserts);
	}

	/**
	 * Execute an INSERT query.
	 *
	 * @param  array			Rows being inserted
	 * @param  ID_TEXT		Table name we're inserting into
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @param  boolean		Whether to get the autoincrement ID created for an insert query
	 * @param  ?integer		The random key that we would use (NULL: not generated yet)
	 * @param  boolean		Whether we are saving as a 'volatile' file extension
	 * @return ?mixed			The insert ID (NULL: not requested / error)
	 */
	function _do_query_insert__execute($inserts,$table_name,$query,$db,$fail_ok,$get_insert_id,&$random_key,$save_as_volatile=false)
	{
		global $TABLE_BASES;

		// Execute
		foreach ($inserts as $record_num=>$record)
		{
			$insert_id=NULL;
			$schema=$this->_read_schema($db,$table_name,$fail_ok);
			if (is_null($schema)) return NULL;
			$no_key_conflict_check=false;
			foreach ($schema as $key=>$val)
			{
				if (!array_key_exists($key,$record)) // Possibly an auto-generated key
				{
					if (preg_replace('#[^\w]#','',$val)=='AUTO')
					{
						$record[$key]=isset($TABLE_BASES[$table_name])?$TABLE_BASES[$table_name]:$this->db_get_first_id(); // We always want first record as '1', because we often reference it in a hard-coded way
						while ((file_exists($db[0].'/'.$table_name.'/'.strval($record[$key]).'.xml')) || (file_exists($db[0].'/'.$table_name.'/'.$this->_guid($schema,$record).'.xml')) || (file_exists($db[0].'/'.$table_name.'/'.strval($record[$key]).'.xml-volatile')) || (file_exists($db[0].'/'.$table_name.'/'.$this->_guid($schema,$record).'.xml-volatile')))
						{
							if ($GLOBALS['IN_MINIKERNEL_VERSION']==1) // In particular the f_groups/f_categories/calendar_types usage of tables references ID numbers for things. But let's just make all installer stuff linear
							{
								$record[$key]++;
								$TABLE_BASES[$table_name]=$record[$key]+1;
							} else
							{
								if ($record_num!=0) $random_key=mt_rand(0,min(2147483647,mt_getrandmax()));
								$record[$key]=$random_key; // We don't use auto-increment, we use randomisation. As otherwise when people sync over revision control there'd be conflicts
							}
						}
						$insert_id=$record[$key];
						if ($val=='*AUTO') $no_key_conflict_check=true;
					} else
					{
						return $this->_bad_query($query,false,'No default value provided for '.$key);
					}
				}
			}
			$guid=$this->_guid($schema,$record);
			$this->_type_check($schema,$record,$query);
			if (!$no_key_conflict_check)
			{
				if ($this->_key_conflict_check($db,$table_name,$schema,$record,$query,$fail_ok))
					return $this->_bad_query($query,$fail_ok,'A record already exists with this key');
			}
			$this->_write_record($db,$table_name,$guid,$record,$fail_ok,$save_as_volatile);
		}

		return $get_insert_id?$insert_id:NULL;
	}

	/**
	 * Parse an SQL expression.
	 *
	 * @param  integer		Our offset counter
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  boolean		Whether to work as a connection point to seek out logic connection expression parts
	 * @param  boolean		Whether to work as a connection point to seek out arithmetic connection expression parts
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return ?array			The expression (NULL: error)
	 */
	function _parsing_read_expression(&$at,$tokens,$query,$db,$look_for_connectives=true,$look_for_any_connectives=true,$fail_ok=false)
	{
		$token=$this->_parsing_read($at,$tokens,$query);
		$expr=array();
		$doing_not=false;
		switch ($token)
		{
			case 'REPLACE':
				if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
				$expr1=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
				if (!$this->_parsing_expects($at,$tokens,',',$query)) return NULL;
				$expr2=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
				if (!$this->_parsing_expects($at,$tokens,',',$query)) return NULL;
				$expr3=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
				if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
				$expr=array('REPLACE',$expr1,$expr2,$expr3);
				break;

			case 'CONCAT':
				if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
				$expr1=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
				if (!$this->_parsing_expects($at,$tokens,',',$query)) return NULL;
				$expr2=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
				if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
				$expr=array('CONCAT',$expr1,$expr2);
				break;

			case 'LENGTH':
				if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
				$expr1=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
				if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
				$expr=array('LENGTH',$expr1);
				break;

			case 'EXISTS':
				if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
				$results=$this->_do_query_select($tokens,$query,$db,1,0,$fail_ok,$at,false);
				if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
				$expr=array('EXISTS',$results);
				break;

			case '(':
				$expr=array('BRACKETED',$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok));
				if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
				break;
			case ')':
				$at--;
				break;

			case '"':
			case "'":
				$token=$this->_parsing_read($at,$tokens,$query);
				$expr=array('LITERAL',$token);
				if ((!$this->_parsing_expects($at,$tokens,"'",$query)) && (!$this->_parsing_expects($at,$tokens,'"',$query))) return NULL;
				break;

			case 'NULL':
				$expr=array('NULL');
				break;

			case 'NOT':
				$expr=array('NOT',$this->_parsing_read_expression($at,$tokens,$query,$db,false,true,$fail_ok));
				break;

			default: // infix op
				$all_keywords=_get_sql_keywords();
				if (!in_array($token,$all_keywords)) // Must be a field reference then
				{
					if (is_numeric($token))
					{
						if (strpos($token,'.')!==false)
						{
							$expr=array('LITERAL',floatval($token));
						} else
						{
							$expr=array('LITERAL',intval($token));
						}
					} else
					{
						$expr=array('FIELD',$token);
					}
				} elseif ($token=='-')
				{
					$token=$this->_parsing_read($at,$tokens,$query);
					if (strpos($token,'.')!==false)
					{
						$expr=array('LITERAL',-floatval($token));
					} else
					{
						$expr=array('LITERAL',-intval($token));
					}
				} else
				{
					$this->_bad_query($query,false,'Unexpected token ('.$token.') in expression');
				}

				// Find the operation now linking across (NB: We're not implementing BODMAS, we assume SQL calculations are very simple and this we'll just do ltr order)
				if ($look_for_any_connectives)
				{
					$token=$this->_parsing_read($at,$tokens,$query,true);

					if ($token=='NOT')
					{
						$doing_not=true;
						$token=$this->_parsing_read($at,$tokens,$query,true);
					}

					switch ($token)
					{
						case '+':
						case '-':
						case '*':
						case '/':
						case '>':
						case '<':
						case '>=':
						case '<=':
						case '=':
						case '<>':
						case 'LIKE':
							$expr=array($token,$expr,$this->_parsing_read_expression($at,$tokens,$query,$db,false,true,$fail_ok));
							break;

						case 'IS':
							$token=$this->_parsing_read($at,$tokens,$query);
							if ($token=='NULL')
							{
								$expr=array('IS_NULL',$expr);
							} else
							{
								$at--;
								if (!$this->_parsing_expects($at,$tokens,'NOT',$query)) return NULL;
								if (!$this->_parsing_expects($at,$tokens,'NULL',$query)) return NULL;
								$expr=array('IS_NOT_NULL',$expr);
							}
							break;

						case 'BETWEEN':
							$expr1=$this->_parsing_read_expression($at,$tokens,$query,$db,false,true,$fail_ok);
							if (!$this->_parsing_expects($at,$tokens,'AND',$query)) return NULL;
							$expr2=$this->_parsing_read_expression($at,$tokens,$query,$db,false,true,$fail_ok);
							$expr=array('BETWEEN',$expr,$expr1,$expr2);
							break;

						case 'IN':
							if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
							$token=$this->_parsing_read($at,$tokens,$query);
							$at--;
							$or_list=array();
							if ($token=='SELECT')
							{
								$results=$this->_do_query_select($tokens,$query,$db,NULL,0,$fail_ok,$at,false);
								foreach ($results as $result)
								{
									$result=array_values($result);
									$or_list[]=$result[0];
								}
							} else
							{
								do
								{
									$expr_in=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
									if (is_null($expr_in)) // Force an exit
									{
										break;
									}
									$or_list[]=$expr_in;
									$token=$this->_parsing_read($at,$tokens,$query);
								}
								while ($token==',');
								$at--;
							}
							if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
							$expr=array('IN',$expr,$or_list);
							break;

						default:
							if (!is_null($token)) $at--;
							break;
					}

					if ($doing_not)
						$expr=array('NOT',$expr);
				}

				break;
		}

		// More connectives?
		if ($look_for_connectives)
		{
			$token=$this->_parsing_read($at,$tokens,$query,true);
			if (!is_null($token))
			{
				switch ($token)
				{
					case 'AND':
						$expr=array($token,$expr,$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok));
						break;

					case 'OR':
						return array($token,$expr,$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok)); // A bit of precedence, causes a fork

					default:
						$at--;
						break;
				}
			}
		}

		return $expr;
	}

	/**
	 * Execute an expression.
	 *
	 * @param  array			The expression
	 * @param  array			Bindings available in the execution scope
	 * @param  string			Query that was executed
	 * @return ?mixed			The result (NULL: error/NULL)
	 */
	function _execute_expression($expr,$bindings,$query)
	{
		switch ($expr[0])
		{
			case 'BRACKETED':
				return $this->_execute_expression($expr[1],$bindings,$query);

			case 'LITERAL':
				return $expr[1];

			case 'NULL':
				return NULL;

			case 'FIELD':
				//if (!array_key_exists($expr[1],$bindings)) {@var_dump($bindings);exit($expr[1]);}	// Useful for debugging
				return $bindings[$expr[1]];

			case '+':
				return $this->_execute_expression($expr[1],$bindings,$query)+$this->_execute_expression($expr[2],$bindings,$query);

			case '-':
				return $this->_execute_expression($expr[1],$bindings,$query)-$this->_execute_expression($expr[2],$bindings,$query);

			case '*':
				return $this->_execute_expression($expr[1],$bindings,$query)*$this->_execute_expression($expr[2],$bindings,$query);

			case '/':
				return $this->_execute_expression($expr[1],$bindings,$query)/$this->_execute_expression($expr[2],$bindings,$query);

			case '>':
				return $this->_execute_expression($expr[1],$bindings,$query)>$this->_execute_expression($expr[2],$bindings,$query);

			case '<':
				return $this->_execute_expression($expr[1],$bindings,$query)<$this->_execute_expression($expr[2],$bindings,$query);

			case '>=':
				return $this->_execute_expression($expr[1],$bindings,$query)>=$this->_execute_expression($expr[2],$bindings,$query);

			case '<=':
				return $this->_execute_expression($expr[1],$bindings,$query)<=$this->_execute_expression($expr[2],$bindings,$query);

			case '=':
				$a=$this->_execute_expression($expr[1],$bindings,$query);
				$b=$this->_execute_expression($expr[2],$bindings,$query);
				if (($expr[1][0]=='FIELD') && ($expr[1][0]=='FIELD')) // Joins between non-equiv-typed fields
				{
					if ((is_integer($a)) && (!is_integer($b))) $a=strval($a);
					elseif ((!is_integer($a)) && (is_integer($b))) $b=strval($b);
				}
				return $a==$b;

			case '<>':
				return $this->_execute_expression($expr[1],$bindings,$query)!=$this->_execute_expression($expr[2],$bindings,$query);

			case 'LIKE':
				$value=$this->_execute_expression($expr[1],$bindings,$query);
				$expr_eval=$this->_execute_expression($expr[2],$bindings,$query);
				return simulated_wildcard_match($value,$expr_eval,true);

			case 'EXISTS':
				return count($expr[1])!=0;

			case 'NOT':
				return !$this->_execute_expression($expr[1],$bindings,$query);

			case 'AND':
				return $this->_execute_expression($expr[1],$bindings,$query) && $this->_execute_expression($expr[2],$bindings,$query);

			case 'OR':
				return $this->_execute_expression($expr[1],$bindings,$query) || $this->_execute_expression($expr[2],$bindings,$query);

			case 'IS_NULL':
				return is_null($this->_execute_expression($expr[1],$bindings,$query));

			case 'IS_NOT_NULL':
				return !is_null($this->_execute_expression($expr[1],$bindings,$query));

			case 'BETWEEN':
				$comp=$this->_execute_expression($expr[1],$bindings,$query);
				return $comp>=$this->_execute_expression($expr[2],$bindings,$query) && $comp<=$this->_execute_expression($expr[3],$bindings,$query);

			case 'REPLACE':
				return str_replace($this->_execute_expression($expr[2],$bindings,$query),$this->_execute_expression($expr[3],$bindings,$query),$this->_execute_expression($expr[1],$bindings,$query));

			case 'CONCAT':
				return $this->_execute_expression($expr[1],$bindings,$query).$this->_execute_expression($expr[2],$bindings,$query);

			case 'LENGTH':
				return strlen($this->_execute_expression($expr[1],$bindings,$query));

			case 'IN':
				$val=$this->_execute_expression($expr[1],$bindings,$query);
				foreach ($expr[2] as $in)
				{
					if ($val==$in) return true;
				}
				return false;
		}

		$this->_bad_query($query,false,'Internal error evaluating expression, '.$expr[0].' not recognised in evaluation context');
	}

	/**
	 * Execute an UPDATE query.
	 *
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  ?integer		The maximum number of rows to affect (NULL: no limit)
	 * @param  ?integer		The start row to affect (NULL: no specification)
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return ?mixed			The results (NULL: no results)
	 */
	function _do_query_update($tokens,$query,$db,$max,$start,$fail_ok)
	{
		// Parse
		$at=0;
		if (!$this->_parsing_expects($at,$tokens,'UPDATE',$query)) return NULL;
		$table_name=$this->_parsing_read($at,$tokens,$query);
		if (!$this->_parsing_expects($at,$tokens,'SET',$query)) return NULL;
		$set=array();
		do
		{
			$token=$this->_parsing_read($at,$tokens,$query);

			if (!$this->_parsing_expects($at,$tokens,'=',$query)) return NULL;
			$expr=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
			if (is_null($expr)) // Force an exit
			{
				break;
			}

			$set[$token]=$expr;

			$token=$this->_parsing_read($at,$tokens,$query,true);
		}
		while ($token===',');
		if (!is_null($token)) $at--;
		$token=$this->_parsing_read($at,$tokens,$query,true);
		if ($token==='WHERE')
		{
			$where_expr=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
		} else
		{
			$where_expr=array('LITERAL',true);
			if (!is_null($token)) $at--;
		}

		// Execute
		$schema=$this->_read_schema($db,$table_name,$fail_ok);
		if (is_null($schema)) return NULL;
		$records=$this->_read_all_records($db,$table_name,'',$schema,$where_expr,$fail_ok,$query);
		if (is_null($records)) return NULL;
		$i=0;
		$done=0;
		foreach ($records as $guid=>$record)
		{
			if (!is_string($guid)) $guid=strval($guid); // As PHP can use type for array keys
			$test=$this->_execute_expression($where_expr,$record,$query);
			if ($test)
			{
				if ($i>=$start)
				{
					$record_new=array();
					foreach ($set as $column_name=>$expr)
					{
						$record_new[$column_name]=$this->_execute_expression($expr,$record,$query);
					}
					$this->_type_check($schema,$record_new,$query);
					$record=$record_new+$record;
					if ($this->_key_conflict_check($db,$table_name,$schema,$record,$query,$fail_ok,$guid))
						return $this->_bad_query($query,$fail_ok,'A record already exists with a key we are updating to');
					$this->_write_record($db,$table_name,$guid,$record,$fail_ok);
					$done++;
					if ((!is_null($max)) && ($done>$max)) break;
				}
				$i++;
			}
		}

		if (!$this->_parsing_check_ended($at,$tokens,$query)) return NULL;
	}

	/**
	 * Execute a DELETE query.
	 *
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  ?integer		The maximum number of rows to affect (NULL: no limit)
	 * @param  ?integer		The start row to affect (NULL: no specification)
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return ?mixed			The results (NULL: no results)
	 */
	function _do_query_delete($tokens,$query,$db,$max,$start,$fail_ok)
	{
		// Parse
		$at=0;
		if (!$this->_parsing_expects($at,$tokens,'DELETE',$query)) return NULL;
		if (!$this->_parsing_expects($at,$tokens,'FROM',$query)) return NULL;
		$table_name=$this->_parsing_read($at,$tokens,$query);
		$token=$this->_parsing_read($at,$tokens,$query,true);
		if ($token==='WHERE')
		{
			$where_expr=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
		} else
		{
			$where_expr=array('LITERAL',true);
			if (!is_null($token)) $at--;
		}

		// Execute
		$schema=$this->_read_schema($db,$table_name,$fail_ok);
		if (is_null($schema)) return NULL;
		$records=$this->_read_all_records($db,$table_name,'',$schema,$where_expr,$fail_ok,$query);
		if (is_null($records)) return NULL;
		$i=0;
		$done=0;
		foreach ($records as $guid=>$record)
		{
			if (!is_string($guid)) $guid=strval($guid); // As PHP can use type for array keys
			$test=$this->_execute_expression($where_expr,$record,$query);
			if ($test)
			{
				if ($i>=$start)
				{
					$path=$db[0].'/'.$table_name.'/'.$guid.'.xml-volatile';
					if (!file_exists($path))
						$path=$db[0].'/'.$table_name.'/'.$guid.'.xml';
					$this->_delete_record($path,$db);
					unset($GLOBALS['DIR_CONTENTS_CACHE'][$table_name]);
					$done++;
					if ((!is_null($max)) && ($done>$max)) break;
				}
				$i++;
			}
		}

		if (!$this->_parsing_check_ended($at,$tokens,$query)) return NULL;
	}

	/**
	 * Execute a SELECT query.
	 *
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  ?integer		The maximum number of rows to affect (NULL: no limit)
	 * @param  ?integer		The start row to affect (NULL: no specification)
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @param  integer		Our offset counter
	 * @param  boolean		Whether to not do the check to make sure we've parsed everything
	 * @return ?mixed			The results (NULL: no results)
	 */
	function _do_query_select($tokens,$query,$db,$max,$start,$fail_ok,&$at,$do_end_check=true)
	{
		// Parse
		if (!$this->_parsing_expects($at,$tokens,'SELECT',$query)) return NULL;
		$select=array();
		do
		{
			$token=$this->_parsing_read($at,$tokens,$query);
			if ($token=='*')
			{
				$select[]=array('*');
			}
			else
			{
				switch ($token)
				{
					case 'COALESCE':
						if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
						$expr1=$this->_parsing_read_expression($at,$tokens,$query,$db,false,true,$fail_ok);
						if (!$this->_parsing_expects($at,$tokens,',',$query)) return NULL;
						$expr2=$this->_parsing_read_expression($at,$tokens,$query,$db,false,true,$fail_ok);
						if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
						$token=array('COALESCE',$expr1,$expr2);
						break;
					case 'DISTINCT':
						$d=$this->_parsing_read($at,$tokens,$query);
						if ($d=='(')
						{
							$d=$this->_parsing_read($at,$tokens,$query);
							if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
						}
						$token=array('DISTINCT',$d);
						break;
					case 'COUNT':
						if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
						$token=array($token,$this->_parsing_read($at,$tokens,$query));
						if ($token[1]=='DISTINCT')
						{
							$token[1]=array('DISTINCT',$this->_parsing_read($at,$tokens,$query));
						}
						if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
						break;
					case 'MAX':
					case 'MIN':
					case 'SUM':
					case 'AVG':
						if (!$this->_parsing_expects($at,$tokens,'(',$query)) return NULL;
						$token=array($token);
						$next=$this->_parsing_read($at,$tokens,$query);
						if ($next=='DISTINCT')
						{
							$distinct=true;
						} else
						{
							$at--;
							$distinct=false;
						}
						$expr=$this->_parsing_read_expression($at,$tokens,$query,$db,false,true,$fail_ok);
						if ($distinct)
						{
							$token[1]=array('DISTINCT',$expr);
						} else
						{
							$token[1]=$expr;
						}
						if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
						break;
				}

				$as_token=$this->_parsing_read($at,$tokens,$query);
				if ($as_token=='AS')
				{
					$as=$this->_parsing_read($at,$tokens,$query);
					$select[]=array('AS',$token,$as);
				}
				elseif (($as_token=='*') && (substr($token,-1)=='.'))
				{
					$select[]=array('*',substr($token,0,strlen($token)-1));
				} else
				{
					$at--;
					$select[]=array('SIMPLE',$token);
				}
			}

			$token=$this->_parsing_read($at,$tokens,$query);
		}
		while ($token==',');
		$at--;

		if (!$this->_parsing_expects($at,$tokens,'FROM',$query)) return NULL;
		$table_name=$this->_parsing_read($at,$tokens,$query);
		if ($table_name=='(')
		{
			$closing_brackets_needed=1;
			$table_name=$this->_parsing_read($at,$tokens,$query);
		} else
		{
			$closing_brackets_needed=0;
		}
		$as_test=$this->_parsing_read($at,$tokens,$query,true);
		if ((!is_null($as_test)) && ($as_test!='ON') && ($as_test!=')') && ($as_test!='LIMIT') && ($as_test!='GROUP') && ($as_test!='ORDER') && ($as_test!='WHERE') && ($as_test!='LEFT') && ($as_test!='RIGHT') && ($as_test!='INNER') && ($as_test!='JOIN'))
		{
			$as=$as_test;
		} else
		{
			$as=$table_name;
			if (!is_null($as_test)) $at--;
		}

		for ($i=0;$i<$closing_brackets_needed;$i++)
		{
			$br=$this->_parsing_read($at,$tokens,$query,true);
			if ($br===')')
			{
				$i--;
				$closing_brackets_needed--;
			} else
			{
				$at--;
				break;
			}
		}

		$joins=array(array('SIMPLE',$table_name,$as));
		do
		{
			$test=$this->_read_join($at,$tokens,$query,$db,$fail_ok,$closing_brackets_needed);
			if (!is_null($test)) $joins[]=$test;
		}
		while (!is_null($test));

		for ($i=0;$i<$closing_brackets_needed;$i++)
		{
			if (!$this->_parsing_expects($at,$tokens,')',$query)) return NULL;
		}

		$token=$this->_parsing_read($at,$tokens,$query,true);
		if ($token==='WHERE')
		{
			$where_expr=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
		} else
		{
			$where_expr=array('LITERAL',true);
			if (!is_null($token)) $at--;
		}
		$token=$this->_parsing_read($at,$tokens,$query,true);
		if ($token==='GROUP')
		{
			if (!$this->_parsing_expects($at,$tokens,'BY',$query)) return NULL;
			$group_by=array();
			do
			{
				$group_by[]=$this->_parsing_read($at,$tokens,$query);
				$test=$this->_parsing_read($at,$tokens,$query,true);
			}
			while ($test===',');
			if (!is_null($test)) $at--;
		} else
		{
			$group_by=NULL;
			if (!is_null($token)) $at--;
		}
		$token=$this->_parsing_read($at,$tokens,$query,true);
		if ($token==='ORDER')
		{
			if (!$this->_parsing_expects($at,$tokens,'BY',$query)) return NULL;
			$orders='';
			do
			{
				$order=$this->_parsing_read($at,$tokens,$query);
				$token=$this->_parsing_read($at,$tokens,$query,true);
				$reverse=false;
				if (($token==='ASC') || ($token==='DESC') || ($token===',') || (!array_key_exists($token,$GLOBALS['DELIMITERS_FLIPPED'])))
				{
					if ($token=='DESC') $reverse=true;
					if ($token==',') $at--;
				} else
				{
					if (($token=='LIMIT') || ($token=='UNION'))
					{
						if (!is_null($token)) $at--;
					} elseif (!is_null($token))
					{
						// Ignore complex order bys
						$orders=NULL;
						$token=NULL;
						$at=count($tokens);
						break;
					}
				}
				if ($orders!='') $orders.=',';
				if ($reverse) $orders.='!';
				$orders.=$order;
				$test=$this->_parsing_read($at,$tokens,$query,true);
			}
			while ($test===',');
			if (!is_null($token)) $at--;
		} else
		{
			$orders=NULL;
			if (!is_null($token)) $at--;
		}

		$token=$this->_parsing_read($at,$tokens,$query,true);
		if (!is_null($token))
		{
			if ($token=='LIMIT')
			{
				$max=intval($this->_parsing_read($at,$tokens,$query));
				$token=$this->_parsing_read($at,$tokens,$query,true);
				if (!is_null($token))
				{
					if ($token==',')
					{
						$start=$max;
						$max=intval($this->_parsing_read($at,$tokens,$query));
					} else
					{
						$at--;
					}
				}
			} else
			{
				$at--;
			}
		}

		// Execute
		$done=0;
		if ((count($joins)==1) && ($where_expr==array('LITERAL',true)) && ($select===array(array('SIMPLE',array('COUNT','*'))))) // Quick fudge to get fast table counts
		{
			global $DIR_CONTENTS_CACHE;
			if (!isset($DIR_CONTENTS_CACHE[$joins[0][1]]))
			{
				@chdir($db[0].'/'.$joins[0][1]);
				$dh=@glob('{,.}*.{xml,xml-volatile}',GLOB_NOSORT|GLOB_BRACE);
				if ($dh===false) $dh=array();
				@chdir(get_file_base());
				if (file_exists($db[0].'/'.$joins[0][1].'/.xml'))
					$dh[]='.xml';
				elseif (file_exists($db[0].'/'.$joins[0][1].'/.xml-volatile'))
					$dh[]='.xml-volatile';
				$DIR_CONTENTS_CACHE[$joins[0][1]]=$dh;
			} else
			{
				$dh=$DIR_CONTENTS_CACHE[$joins[0][1]];
			}
			$records=array();
			foreach ($dh as $d)
			{
				$records[]=array('file'=>$d);
			}
		} else
		{
			foreach ($joins as $join)
			{
				if ($join[0]=='SIMPLE')
				{
					$joined_as=$join[2];

					$schema=$this->_read_schema($db,$join[1],$fail_ok);

					if (is_null($schema)) return NULL;
					$records=$this->_read_all_records($db,$join[1],$joined_as,$schema,$where_expr,$fail_ok,$query);
					if (is_null($records)) return NULL;

					foreach ($schema as $k=>$v)
					{
						$schema[$joined_as.'.'.$k]=$v; // Needed so all scoped variables can be put in place as NULL's in a right variable
					}

					// Handle the join as condition
					foreach ($records as $guid=>$record)
					{
						if (!is_string($guid)) $guid=strval($guid); // As PHP can use type for array keys
						$new_record=$record;
						foreach ($record as $key=>$val)
						{
							$new_record[$joined_as.'.'.$key]=$val;
						}
						$records[$guid]=$new_record;
					}
				} else
				{
					$result=$this->_execute_join($db,$as,$join,$query,$records,$schema,$where_expr,$fail_ok);
					if (is_null($result)) return NULL;
					list($records,$schema)=$result;
				}
			}
		}
		if (!is_null($group_by))
		{
			$record_sets=array();
			foreach ($records as $record)
			{
				$s=array();
				foreach ($group_by as $v)
					$s[]=$record[$v];
				if (!array_key_exists(serialize($s),$record_sets)) $record_sets[serialize($s)]=array();
				$record_sets[serialize($s)][]=$record;
			}
			$records=array();
			foreach ($record_sets as $set) // Functions have special meaning in GROUP BY, and we need to compute them in the group-aware scope
			{
				$rep=$this->_function_set_scoping($set,$select,$set[0],$query);
				$records[]=$rep;
			}
		} else
		{
			// Special handling for DISTINCT
			foreach ($select as $s)
			{
				if ((array_key_exists(1,$s)) && ($s[0]!='*'))
				{
					$s_term=$s[1];
					if ($s[0]=='SIMPLE') $s_as=$s[1];
					else $s_as=$s[2];
					if (is_array($s_term))
					{
						switch ($s_term[0])
						{
							case 'DISTINCT':
								$index=array();
								foreach ($records as $set_item)
								{
									$val=$set_item[$s_term[1]];
									$index[$val]=$set_item;
								}
								$records=array_values($index);
								break;
						}
					}
				}
			}

			// Now handle functions
			$single_result=false;
			foreach ($select as $s)
			{
				if ((array_key_exists(1,$s)) && ($s[0]!='*') && (is_array($s[1])))
				{
					if (($s[1][0]=='MIN') || ($s[1][0]=='MAX') || ($s[1][0]=='SUM') || ($s[1][0]=='COUNT') || ($s[1][0]=='AVG'))
					{
						$single_result=true;
					}
				}
			}
			foreach ($records as $i=>$record)
			{
				$records[$i]=$this->_function_set_scoping($records,$select,$record,$query);
				if ($single_result)
				{
					$records=array($i=>$records[$i]);
					break;
				}
			}
		}

		// Filter
		$pre_filtered_records=array();
		foreach ($records as $record)
		{
			$test=$this->_execute_expression($where_expr,$record,$query);
			if ($test)
			{
				$pre_filtered_records[]=$record;
			}
		}

		// Sort
		if (!is_null($orders))
		{
			global $M_SORT_KEY;
			$M_SORT_KEY=$orders;
			usort($pre_filtered_records,'multi_sort');
		}

		// Cut
		$i=0;
		$filtered_records=array();
		foreach ($pre_filtered_records as $record)
		{
			if ($i>=$start)
			{
				$filtered_records[]=$record;
				$done++;
				if ((!is_null($max)) && ($done>=$max)) break;
			}
			$i++;
		}

		// Select
		$results=array();
		foreach ($filtered_records as $record)
		{
			$_record=array();
			foreach ($select as $want)
			{
				switch ($want[0]) // NB: COUNT, SUM, etc, already have their values rolled out into $record and we do not need to consider it here
				{
					case '*':
						if (array_key_exists(1,$want))
						{
							$filtered_record=array();
							foreach ($record as $key=>$val)
							{
								if (substr($key,0,strlen($want[1].'.'))==$want[1].'.')
									$filtered_record[substr($key,strlen($want[1].'.'))]=$val;
							}
							$_record+=$filtered_record;
						} else
						{
							$filtered_record=array();
							foreach ($record as $key=>$val)
							{
								if (strpos($key,'.')===false)
									$filtered_record[$key]=$val;
							}
							$_record+=$filtered_record;
						}
						break;

					case 'SIMPLE':
						$param=is_array($want[1])?$want[1][0]:$want[1];
						if ($param=='DISTINCT') $param=is_array($want[1])?$want[1][1]:$want[1];

						if (strpos($param,'.')===false)
						{
							$_record[$param]=$record[$param];
						} else
						{
							$_record[preg_replace('#^.*\.#','',$param)]=$record[$param];
						}

						break;

					case 'AS':
						$_record[$want[2]]=$record[is_array($want[1])?$want[1][0]:$want[1]];
						break;
				}
			}
			$results[]=$_record;
		}

		if ((count($results)==0) && (is_null($group_by))) // If there are no records, but some functions, we need to add a row
		{
			$rep=$this->_function_set_scoping(array(),$select,array(),$query);
			/*foreach (array_keys($schema) as $key)
			{
				$rep[$key]=NULL;
			}*/
			if (count($rep)!=0)
			{
				foreach ($select as $want)
				{
					switch ($want[0]) // NB: COUNT, SUM, etc, already have their values rolled out into $record and we do not need to consider it here
					{
						case 'AS':
							if (isset($rep[is_array($want[1])?$want[1][0]:$want[1]]))
							{
								$old=$rep[is_array($want[1])?$want[1][0]:$want[1]];
								unset($rep[is_array($want[1])?$want[1][0]:$want[1]]);
							} else
							{
								$old=NULL;
							}
							$rep[$want[2]]=$old;
							break;
						case 'SIMPLE':
							if (!isset($rep[is_array($want[1])?$want[1][0]:$want[1]])) $rep[is_array($want[1])?$want[1][0]:$want[1]]=NULL;
							break;
					}
				}

				$results[]=$rep;
			}
		}

		// UNION clause?
		$token=$this->_parsing_read($at,$tokens,$query,true);
		if ($token==='UNION')
		{
			$token=$this->_parsing_read($at,$tokens,$query);
			if ($token=='ALL')
			{
				$de_dupe=false;
			} else
			{
				$de_dupe=true;
				$at--;
			}

			$results_b=$this->_do_query_select($tokens,$query,$db,$max,$start,$fail_ok,$at);

			if ($de_dupe)
			{
				foreach ($results_b as $r)
				{
					if (!in_array($r,$results))
						$results[]=$r;
				}
			} else
			{
				$results=array_merge($results,$results_b);
			}
		} else
		{
			if (!is_null($token)) $at--;
			if ($do_end_check)
			{
				if (!$this->_parsing_check_ended($at,$tokens,$query)) return NULL;
			}
		}

		return $results;
	}

	/**
	 * Run SQL data filter functions over a result set.
	 *
	 * @param  array			The set of results we are operating on
	 * @param  array			Parse tree of what we are selecting
	 * @param  array			Record we are copying the function results into
	 * @param  string			Query that was executed
	 * @return array			The result row based on the set
	 */
	function _function_set_scoping($set,$select,$rep,$query)
	{
		foreach ($select as $s)
		{
			if ((array_key_exists(1,$s)) && ($s[0]!='*'))
			{
				$s_term=$s[1];
				$chosen_param_name=is_array($s[1])?$s[1][0]:$s[1]; // "AS" is handled elsewhere, for now we just store
				if ($chosen_param_name=='DISTINCT') $chosen_param_name=is_array($s[1])?$s[1][1]:$s[1];
				if (is_array($s_term))
				{
					switch ($s_term[0])
					{
						case 'COALESCE':
							$val=$this->_execute_expression($s_term[1],$set[0],$query);
							if (is_null($val)) $val=$this->_execute_expression($s_term[2],$set[0],$query);
							$rep[$chosen_param_name]=$val;
							break;
						case 'MAX':
							$max=mixed();
							foreach ($set as $set_item)
							{
								$val=$this->_execute_expression($s_term[1],$set_item,$query);
								if ((is_null($max)) || ($val>$max)) $max=$val;
							}
							$rep[$chosen_param_name]=$max;
							break;
						case 'MIN':
							$min=mixed();
							foreach ($set as $set_item)
							{
								$val=$this->_execute_expression($s_term[1],$set_item,$query);
								if ((is_null($min)) || ($val<$min)) $min=$val;
							}
							$rep[$chosen_param_name]=$min;
							break;
						case 'COUNT':
							if ($s_term[1][0]=='DISTINCT')
							{
								$index=array();
								foreach ($set as $set_item)
								{
									$val=$set_item[$s_term[1][1]];
									$index[$val]=true;
								}
								$rep[$chosen_param_name]=count($index);
							} else
							{
								$rep[$chosen_param_name]=count($set);
							}
							break;
						case 'SUM':
							$temp=0;
							foreach ($set as $set_item)
							{
								$val=$this->_execute_expression($s_term[1],$set_item,$query);
								$temp+=$val;
							}
							if (is_integer($temp))
							{
								$rep[$chosen_param_name]=floatval($temp);
							} else
							{
								$rep[$chosen_param_name]=$temp;
							}
							break;
						case 'AVG':
							if (count($set)==0)
							{
								$rep[$chosen_param_name]=NULL;
							} else
							{
								$temp=0;
								foreach ($set as $set_item)
								{
									$val=$this->_execute_expression($s_term[1],$set_item,$query);
									$temp+=$val;
								}
								if (is_integer($temp))
								{
									$rep[$chosen_param_name]=floatval($temp)/floatval(count($set));
								} else
								{
									$rep[$chosen_param_name]=$temp/floatval(count($set));
								}
							}
							break;
					}
				}
			}
		}
		return $rep;
	}

	/**
	 * Read in a table specifier clause for a WHERE query.
	 *
	 * @param  integer		Our offset counter
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  array			Database connection
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @param  integer		How many closing brackets we expect
	 * @return ?array			Join condition (NULL: no join here)
	 */
	function _read_join(&$at,$tokens,$query,$db,$fail_ok,&$closing_brackets_needed)
	{
		$token=$this->_parsing_read($at,$tokens,$query,true);

		if (($token!==',') && ($token!=='JOIN') && ($token!=='LEFT') && ($token!=='RIGHT') && ($token!=='INNER'))
		{
			if (!is_null($at)) $at--;
			return NULL;
		}

		if (($token!='JOIN') && ($token!=','))
		{
			if (!$this->_parsing_expects($at,$tokens,'JOIN',$query)) return NULL;
		}

		$join_table=$this->_parsing_read($at,$tokens,$query);

		if ($join_table=='(')
		{
			$closing_brackets_needed++;
			$join_table=$this->_parsing_read($at,$tokens,$query);
		}

		$joined_as_test=$this->_parsing_read($at,$tokens,$query,true);
		if ((!is_null($joined_as_test)) && ($joined_as_test!='ON') && ($joined_as_test!='WHERE') && ($joined_as_test!=',') && ($joined_as_test!='LEFT') && ($joined_as_test!='RIGHT') && ($joined_as_test!='INNER') && ($joined_as_test!='JOIN'))
		{
			if ($joined_as_test=='AS') $joined_as_test=$this->_parsing_read($at,$tokens,$query); // 'AS' is optional
			$joined_as=$joined_as_test;
		} else
		{
			$joined_as=$join_table;
			if (!is_null($joined_as_test)) $at--;
		}

		if ($token==',')
		{
			$on_expr=array('LITERAL',true);
		} else
		{
			if (!$this->_parsing_expects($at,$tokens,'ON',$query)) return NULL;
			$on_expr=$this->_parsing_read_expression($at,$tokens,$query,$db,true,true,$fail_ok);
		}

		for ($i=0;$i<$closing_brackets_needed;$i++)
		{
			$br=$this->_parsing_read($at,$tokens,$query,true);
			if ($br===')')
			{
				$i--;
				$closing_brackets_needed--;
			} else
			{
				$at--;
				break;
			}
		}

		switch ($token)
		{
			case ',':
				$join=array('JOIN',$join_table,$joined_as,$on_expr);
				break;
			case 'JOIN':
				$join=array('JOIN',$join_table,$joined_as,$on_expr);
				break;
			case 'LEFT':
				$join=array('LEFT_JOIN',$join_table,$joined_as,$on_expr);
				break;
			case 'RIGHT':
				$join=array('RIGHT_JOIN',$join_table,$joined_as,$on_expr);
				break;
			case 'INNER':
				$join=array('INNER_JOIN',$join_table,$joined_as,$on_expr);
				break;
		}

		return $join;
	}

	/**
	 * Optimize a join condition into a join scope set, if possible.
	 *
	 * @param  array			Join condition (parsed WHERE-style clause)
	 * @param  array			Schema so far
	 * @param  array			Records so far
	 * @param  string			The renaming of our table, so we can recognise it in the join condition
	 * @return array			Altered join condition
	 */
	function _setify_join_condition_for_optimisation($join_condition,$schema,$records,$joined_as)
	{
		if ($join_condition[0]=='AND')
		{
			$join_condition_a=$this->_setify_join_condition_for_optimisation($join_condition[1],$schema,$records,$joined_as);
			$join_condition_b=$this->_setify_join_condition_for_optimisation($join_condition[2],$schema,$records,$joined_as);
			$join_condition=array('AND',$join_condition_a,$join_condition_b);
		} else
		{
			if ($join_condition[0]=='=')
			{
				foreach (array(1,2) as $i)
				{
					if (($join_condition[$i][0]=='FIELD') && ($join_condition[3-$i][0]=='FIELD'))
					{
						$var=preg_replace('#^'.$joined_as.'\.#','',$join_condition[$i][1]);
						if (array_key_exists($var,$schema))
						{
							$join_condition[$i][1]=array();
							foreach ($records as $r)
							{
								$join_condition[$i][1][]=$r[$var];
							}
							$join_condition[$i][0]='LITERAL';
							break;
						}
					}
				}
			}
		}

		return $join_condition;
	}

	/**
	 * Get results from a JOIN.
	 *
	 * @param  array			Database connection
	 * @param  string			The renaming of our table, so we can recognise it in the join condition
	 * @param  array			Join op-tree
	 * @param  string			Query that was executed
	 * @param  array			Records so far
	 * @param  array			Schema so far
	 * @param  array			Expression filtering results (used for optimisation, seeing if we can get a quick key match)
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return ?array			A pair: an array of results, an array of the schema for what has been joined (NULL: error)
	 */
	function _execute_join($db,$joined_as_prior,$join,$query,$records,$schema,$where_expr,$fail_ok=false)
	{
		$joined_as=$join[2];

		$schema_b=$this->_read_schema($db,$join[1],$fail_ok);
		foreach ($schema_b as $k=>$v)
		{
			$schema_b[$join[2].'.'.$k]=$v; // Needed so all scoped variables can be put in place as NULL's in a right variable
		}
		if (is_null($schema_b)) return NULL;
		$join_condition=$join[3];
		$join_condition=$this->_setify_join_condition_for_optimisation($join_condition,$schema,$records,$joined_as_prior);
		$where_expr=$this->_setify_join_condition_for_optimisation($where_expr,$schema,$records,$joined_as_prior); // Good for implicit joins (,)
		if ($where_expr==array('LITERAL',true))
		{
			$where_expr_combined=$join_condition;
		} else
		{
			$where_expr_combined=array('AND',$where_expr,$join_condition);
		}
		$records_b=$this->_read_all_records($db,$join[1],$joined_as,$schema_b,$where_expr_combined,$fail_ok,$query);
		if (is_null($records_b)) return NULL;

		// Handle the join as condition
		foreach ($records_b as $guid=>$record)
		{
			if (!is_string($guid)) $guid=strval($guid); // As PHP can use type for array keys
			$new_record=$record;
			foreach ($record as $key=>$val)
			{
				$new_record[$joined_as.'.'.$key]=$val;
			}
			$records_b[$guid]=$new_record;
		}

		$records_results=array();

		switch ($join[0])
		{
			case 'JOIN':
			case 'INNER_JOIN':
				foreach ($records as $r1)
				{
					foreach ($records_b as $r2)
					{
						$join_scope=$r1;
						foreach ($r2 as $key=>$val)
						{
							if (array_key_exists($key,$join_scope)) // Don't allow anything ambiguous
							{
								unset($join_scope[$key]);
							} else
							{
								$join_scope[$key]=$val;
							}
						}
						$test=$this->_execute_expression($join[3],$join_scope,$query);
						if ($test)
							$records_results[]=$r2+$r1;
					}
				}
				break;

			case 'RIGHT_JOIN':
				foreach ($records_b as $r1)
				{
					$matched=false;
					foreach ($records as $r2)
					{
						$join_scope=$r1;
						foreach ($r2 as $key=>$val)
						{
							if (array_key_exists($key,$join_scope)) // Don't allow anything ambiguous
							{
								unset($join_scope[$key]);
							} else
							{
								$join_scope[$key]=$val;
							}
						}
						$test=$this->_execute_expression($join[3],$join_scope,$query);
						if ($test)
						{
							$records_results[]=$r2+$r1;
							$matched=true;
						}
					}
					if (!$matched)
					{
						$null_padded=$r1;
						foreach (array_keys($schema) as $field)
						{
							$null_padded[$field]=NULL;
						}
						$records_results[]=$null_padded;
					}
				}
				break;
			case 'LEFT_JOIN':
				foreach ($records as $r1)
				{
					$matched=false;
					foreach ($records_b as $r2)
					{
						$join_scope=$r1;
						foreach ($r2 as $key=>$val)
						{
							if (array_key_exists($key,$join_scope)) // Don't allow anything ambiguous
							{
								unset($join_scope[$key]);
							} else
							{
								$join_scope[$key]=$val;
							}
						}
						$test=$this->_execute_expression($join[3],$join_scope,$query);
						if ($test)
						{
							$records_results[]=$r2+$r1;
							$matched=true;
						}
					}
					if (!$matched)
					{
						$null_padded=$r1;
						foreach (array_keys($schema_b) as $field)
						{
							$null_padded[$field]=NULL;
						}
						$records_results[]=$null_padded;
					}
				}
				break;
		}

		foreach ($schema_b as $k=>$v)
		{
			$schema[$k]=$v;
		}

		return array($records_results,$schema);
	}

	/**
	 * Reads the next token.
	 *
	 * @param  integer		Our offset counter
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  boolean		Whether it can return NULL if we're out of output (otherwise fails)
	 * @return ?string		Token read (NULL: error, read too far)
	 */
	function _parsing_read(&$at,$tokens,$query,$fail_ok=false)
	{
		$at++;

		if (!array_key_exists($at-1,$tokens))
		{
			if ($fail_ok) return NULL;
			return $this->_bad_query($query,false,'Unexpected end of query');
		}

		return preg_replace('#^`(.*)`$#','${1}',$tokens[$at-1]);
	}

	/**
	 * Expect a certain token next.
	 *
	 * @param  integer		Our offset counter
	 * @param  array			Tokens
	 * @param  string			Token expected
	 * @param  string			Query that was executed
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return boolean		Success status
	 */
	function _parsing_expects(&$at,$tokens,$token,$query,$fail_ok=false)
	{
		$next=$this->_parsing_read($at,$tokens,$query,$fail_ok);
		if ($next!=$token)
		{
			$this->_bad_query($query,$fail_ok,'Expected '.$token.' but got '.$next.' at token '.strval($at));
			return false;
		}
		return true;
	}

	/**
	 * Check we've consumed all our tokens.
	 *
	 * @param  integer		Our offset counter
	 * @param  array			Tokens
	 * @param  string			Query that was executed
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @return boolean		Success status
	 */
	function _parsing_check_ended($at,$tokens,$query,$fail_ok=false)
	{
		do
		{
			$token=$this->_parsing_read($at,$tokens,$query,true);
		}
		while ($token===';');
		if (!is_null($token))
		{
			$this->_bad_query($query,$fail_ok,'Extra unexpected tokens in query at token #'.strval($at+1).', "'.$token.'"');
			return false;
		}
		return true;
	}

	/**
	 * Give out an error message and die, when a query fails.
	 *
	 * @param  string			The query that failed
	 * @param  boolean		Whether to not output an error on some kind of run-time failure (parse errors and clear programming errors are always fatal)
	 * @param  ?string		Error message (NULL: none)
	 * @return ?mixed			Always returns null (NULL: error)
	 */
	function _bad_query($query,$fail_ok=false,$error=NULL)
	{
		if (!$fail_ok)
		{
			$msg='Failed on query: '.$query;
			if (!is_null($error))
			{
				$msg.=' ['.$error.']';
			}
			fatal_exit($msg);
		}
		return NULL;
	}

	/**
	 * Generate a GUID for a record, preferably from the key, but doesn't have to be.
	 *
	 * @param  ?array			The schema (NULL: don't have/use)
	 * @param  ?array			The record (NULL: don't have/use)
	 * @return string			The GUID
	 */
	function _guid($schema=NULL,$record=NULL)
	{
		if ((!is_null($schema)) && (!is_null($record)))
		{
			$guid='';
			ksort($schema);
			$whole_key=true;
			foreach ($schema as $key=>$type)
			{
				if (strpos($type,'*')!==false)
				{
					if (array_key_exists($key,$record))
					{
						$val=$record[$key];

						if ($guid!='')
							$guid.=',';
						$new_val='';
						if ((is_array($val)) && (count($val)==1))
						{
							$val=$val[0];
						}
						if (is_string($val))
						{
							$new_val=$val;
						}
						elseif (is_integer($val))
						{
							$new_val=strval($val);
						}
						elseif (is_float($val))
						{
							$new_val=float_to_raw_string($val);
						}
						if ($key=='id')
						{
							$guid.=$this->_escape_name($new_val);
						} else
						{
							$guid.=$key.'='.$this->_escape_name($new_val);
						}
					} else
					{
						$whole_key=false;
					}
				}
			}

			if ($whole_key) return $guid;
		}

      $fuzz=strtoupper(md5(uniqid(strval(mt_rand(0,min(2147483647,mt_getrandmax()))),true)));

      return '{'
			.substr($fuzz,0,8).'-'
			.substr($fuzz,8,4).'-'
			.substr($fuzz,12,4).'-'
			.substr($fuzz,16,4).'-'
			.substr($fuzz,20,12)
			.'}';
	}

	/**
	* Escape a value for use in a filesystem path.
	*
	* @param  string	Value to escape (original value)
	* @return string	Escaped value
	*/
	function _escape_name($in)
	{
		return str_replace(array('=',':',',','/','|'),array('!equals!','!colon!','!comma!','!slash!','!pipe!'),$in);
	}

	/**
	* Unescape a value from a filesystem path back to the original.
	*
	* @param  string	Escaped value
	* @return string	Original value
	*/
	function _unescape_name($in)
	{
		return str_replace(array('!equals!','!colon!','!comma!','!slash!','!pipe!'),array('=',':',',','/','|'),$in);
	}

}
