Show:

File: platform/classes/Db/Mysql.php

<?php

/**
 * @module Db
 */

class Db_Mysql implements Db_Interface
{
	/**
	 * This class lets you create and use PDO database connections.
	 * @class Db_Mysql
	 * @extends Db_Interface
	 * @constructor
	 *
	 * @param {string} $connectionName The name of the connection out of the connections added with Db::setConnection()
	 * This is required for actually connecting to the database.
	 * @param {PDO} [$pdo=null] Existing PDO connection. Only accepts connections to MySQL.
	 */
	function __construct ($connectionName, PDO $pdo = null)
	{
		$this->connectionName = $connectionName;
		if ($pdo) {
			// The following statement may throw an exception, which is fine.
			$driver_name = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
			if (strtolower($driver_name) != 'mysql')
				throw new Exception("the PDO object is not for mysql", -1);

			$this->pdo = $pdo;
		}
	}

	/**
	 * The PDO connection that this object uses
	 * @property $pdo
	 * @type PDO
	 */
	public $pdo;
	
	/**
	 * The name of the connection
	 * @property $connectionName
	 * @type string
	 * @protected
	 */
	protected $connectionName;
	
	/**
	 * The name of the shard currently selected with reallyConnect, if any
	 * @property $shardName
	 * @type string
	 * @protected
	 */
	protected $shardName;

	/**
	 * The database name of the shard currently selected with reallyConnect, if any
	 * @property $dbname
	 * @type string
	 */
	public $dbname;

	/**
	 * The prefix of the shard currently selected with reallyConnect, if any
	 * @property $prefix
	 * @type string
	 */
	public $prefix;

	/**
	 * The cutoff after which strlen gets too expensive to check automatically
	 * @property $maxCheckStrlen
	 * @type string
	 */
	public $maxCheckStrlen = 1000000;

	/**
	 * Actually makes a connection to the database (by creating a PDO instance)
	 * @method reallyConnect
	 * @param {array} [$shardName=null] A shard name that was added using Db::setShard.
	 * This modifies how we connect to the database.
	 * @return {PDO} The PDO object for connection
	 */
	function reallyConnect($shardName = null)
	{
		if ($this->pdo) {
			return $this->pdo;
		}
		$connectionName = $this->connectionName;
		$connectionInfo = Db::getConnection($connectionName);
		if (empty($connectionInfo)) {
			throw new Exception("database connection \"$connectionName\" wasn't registered with Db.", -1);
		}
		
		if (empty($shardName)) {
			$shardName = '';
		}
		$modifications = Db::getShard($connectionName, $shardName);
		if (!isset($modifications)) {
			$modifications = array();
		}
		if (class_exists('Q')) {
			/**
			 * Occurs before a real connection to the database is made
			 * @event Db/reallyConnect {before}
			 * @param {Db_Mysql} db
			 * @param {string} shardName
			 * @param {array} modifications
			 * @return {array}
			 *	Extra modifications
			 */
			$more = Q::event('Db/reallyConnect', array(
				'db' => $this,
				'shardName' => $shardName,
				'modifications' => $modifications
			), 'before');
			if ($more) {
				$modifications = array_merge($modifications, $more);
			}
		}
		
		$dsn = isset($modifications['dsn']) ? $modifications['dsn'] : $connectionInfo['dsn'];
		$prefix = isset($modifications['prefix']) ? $modifications['prefix'] : $connectionInfo['prefix'];
		$username = isset($modifications['username']) ? $modifications['username'] : $connectionInfo['username'];
		$password = isset($modifications['password']) ? $modifications['password'] : $connectionInfo['password'];
		$driver_options = isset($modifications['driver_options']) 
			? $modifications['driver_options'] 
			: isset($connectionInfo['driver_options']) ? $connectionInfo['driver_options'] : null;

		// More dsn changes
		$dsn_fields = array();
		foreach (array('host', 'port', 'dbname', 'unix_socket', 'charset') as $f) {
			if (isset($modifications[$f])) {
				$dsn_fields[$f] = $modifications[$f];
			}
		}
		if ($dsn_fields) {
			$dsn_array = array_merge(Db::parseDsnString($dsn), $dsn_fields);
			$dsn = 'mysql:'.http_build_query($dsn_array, '', ';');
		} else {
			$dsn_array = Db::parseDsnString($dsn);
		}

		// The connection may have already been made with these parameters,
		// in which case we will just retrieve the existing connection.
		$this->pdo = Db::pdo($dsn, $username, $password, $driver_options);
		$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
		$this->shardName = $shardName;
		$this->dbname = $dsn_array['dbname'];
		$this->prefix = $prefix;

		if (class_exists('Q')) {
			/**
			 * Occurs when a real connection to the database has been made
			 * @event Db/reallyConnect {after}
			 * @param {Db_Mysql} db
			 * @param {string} shardName
			 * @param {array} modifications
			 */
			Q::event('Db/reallyConnect', array(
				'db' => $this,
				'shardName' => $shardName,
				'modifications' => $modifications
			), 'after');
		}
		$this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
		$this->pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
		$this->setTimezone();
		return $this->pdo;
	}
	
	/**
	 * Sets the timezone in the database to match the one in PHP
	 * @param {integer} [$offset=timezone_offset_get()] in seconds
	 * @method setTimezone
	 */
	function setTimezone($offset = null)
	{
		if (!isset($offset)) {
			$offset = (int)date('Z');
		}
		if (!$offset) {
			$offset = 0;
		}
		$abs = abs($offset);
		$hours = sprintf("%02d", floor($abs / 3600));
		$minutes = sprintf("%02d", floor(($abs % 3600) / 60));
		$sign = ($offset > 0) ? '+' : '-';
		$this->rawQuery("SET time_zone = '$sign$hours:$minutes';")->execute();
	}
	
	/**
	 * Returns the name of the shard currently selected with reallyConnect, if any
	 * @method shardName
	 * @return {string}
	 */
	function shardName()
	{
		return $this->shardName;
	}

	/**
	 * Forwards all other calls to the PDO object
	 * @method __call
	 * @param {string} $name The function name
	 * @param {array} $arguments The arguments
	 * @return {mixed} The result of method call
	 */
	function __call ($name, array $arguments)
	{
		$this->reallyConnect();
		if (!is_callable(array($this->pdo, $name))) {
			throw new Exception("neither Db_Mysql nor PDO supports the $name function");
		}
		return call_user_func_array(array($this->pdo, $name), $arguments);
	}

	/**
	 * Returns the name of the connection with which this Db object was created.
	 * @method connectionName
	 * @return {string}
	 */
	function connectionName ()
	{
		return isset($this->connectionName) ?  $this->connectionName : null;
	}
	
	/**
	 * Returns the connection info with which this Db object was created.
	 * @method connection
	 * @return {string}
	 */
	function connection()
	{
		if (isset($this->connectionName)) {
			return Db::getConnection($this->connectionName);
		}
		return null;
	}
	
	/**
	 * Returns an associative array representing the dsn
	 * @method dsn
	 * @return {array}
	 */
	function dsn()
	{
		$connectionInfo = Db::getConnection($this->connectionName);
		if (empty($connectionInfo['dsn'])) {
			throw new Exception(
				'No dsn string found for the connection ' 
				. $this->connectionName
			);
		}
		return Db::parseDsnString($connectionInfo['dsn']);
	}
	
	/**
	 * Returns the lowercase name of the dbms (e.g. "mysql")
	 * @method dbms
	 * @return {string}
	 */
	function dbms()
	{
		return 'mysql';
	}
	
	/**
	 * Returns the name of the database used
	 * @method dbName
	 * @return {string}
	 */
	function dbName()
	{
		$dsn = $this->dsn();
		if (empty($dsn))
			return null;
		return $dsn['dbname'];
	}

	/**
	 * Creates a query to select fields from a table. Needs to be used with Db_Query::from().
	 * @method select
	 * @param {string|array} [$fields='*'] The fields as strings, or "*", or array of alias=>field
	 * @param {string|array} [$tables=''] The tables as strings, or array of alias=>table
	 * @return {Db_Query_Mysql} The resulting Db_Query object
	 */
	function select ($fields = '*', $tables = '')
	{
		if (empty($fields))
			throw new Exception("fields not specified in call to 'select'.");
		if (!isset($tables))
			throw new Exception("tables not specified in call to 'select'.");
		$query = new Db_Query_Mysql($this, Db_Query::TYPE_SELECT);
		return $query->select($fields, $tables);
	}

	/**
	 * Creates a query to insert a row into a table
	 * @method insert
	 * @param {string} $table_into The name of the table to insert into
	 * @param {array} $fields=array() The fields as an array of column=>value pairs
	 * @return {Db_Query_Mysql} The resulting Db_Query_Mysql object
	 */
	function insert ($table_into, array $fields = array())
	{
		if (empty($table_into))
			throw new Exception("table not specified in call to 'insert'.");
		
		// $fields might be an empty array,
		// but the insert will still be attempted.
		
		$columns_list = array();
		$values_list = array();
		foreach ($fields as $column => $value) {
			$columns_list[] = Db_Query_Mysql::column($column);
			if ($value instanceof Db_Expression) {
				$values_list[] = "$value";
			} else {
				$values_list[] = ":$column";
			}
		}
		$columns_string = implode(', ', $columns_list);
		$values_string = implode(', ', $values_list);
		
		$clauses = array(
			'INTO' => "$table_into ($columns_string)", 'VALUES' => $values_string
		);
		
		return new Db_Query_Mysql($this, Db_Query::TYPE_INSERT, $clauses, $fields, $table_into);
	}

	/**
	 * Inserts multiple rows into a single table, preparing the statement only once,
	 * and executes all the queries.
	 * @method insertManyAndExecute
	 * @param {string} $table_into The name of the table to insert into
	 * @param {array} [$rows=array()] The array of rows to insert. 
	 * Each row should be an array of ($field => $value) pairs, with the exact
	 * same set of keys (field names) in each array. It can also be a Db_Row.
	 * @param {array} [$options=array()] An associative array of options, including:
	 * @param {string} [$options.className]
	 *    If you provide the class name, the system will be able to use any sharding
	 *    indexes under that class name in the config.
	 * @param {integer} [$options.chunkSize]
	 *    The number of rows to insert at a time. Defaults to 20.
	 *    You can also put 0 here, which means unlimited chunks, but it's not recommended.
	 * @param {array} [$options.onDuplicateKeyUpdate]
	 *    You can put an array of fieldname => value pairs here,
	 *    which will add an ON DUPLICATE KEY UPDATE clause to the query.
	 */
	function insertManyAndExecute ($table_into, array $rows = array(), $options = array())
	{
		// Validate and get options
		if (empty($table_into)) {
			throw new Exception("table not specified in call to 'insertManyAndExecute'.");
		}
		if (empty($rows)) {
			return false;
		}
		$chunkSize = isset($options['chunkSize']) ? $options['chunkSize'] : 20;
		if ($chunkSize < 0) {
			return false;
		}
		$onDuplicateKeyUpdate = isset($options['onDuplicateKeyUpdate'])
				? $options['onDuplicateKeyUpdate'] : null;
		$className = isset($options['className']) ? $options['className'] : null;
		
		// Get the columns list
		$row = reset($rows);
		$record = ($row instanceof Db_Row) ? $row->fields : $row;
		foreach ($record as $column => $value) {
			$columns_list[] = Db_Query_Mysql::column($column);
		}
		$columns_string = implode(', ', $columns_list);
		$into = "$table_into ($columns_string)";
		
		// On duplicate key update clause (optional)
		$update_fields = array();
		$odku_clause = '';
		if (isset($onDuplicateKeyUpdate)) {
			$odku_clause = "\n\t ON DUPLICATE KEY UPDATE ";
			$parts = array();
			foreach ($onDuplicateKeyUpdate as $k => $v) {
				if ($v instanceof Db_Expression) {
					$part = "= $v";
				} else {
					$part = " = :__update_$k";
					$update_fields["__update_$k"] = $v;
				}
				$parts[] .= Db_Query_Mysql::column($k) . $part;
			}
			$odku_clause .= implode(",\n\t", $parts);
		}
		
		// Start filling
		$queries = array();
		$queryCounts = array();
		$bindings = array();
		$last_q = array();
		$last_queries = array();
		foreach ($rows as $row) {
			// get shard, if any
			$record = ($row instanceof Db_Row) ? $row->fields : $row;
			$query = new Db_Query_Mysql($this, Db_Query::TYPE_INSERT);
			$shard = '';
			if (isset($className)) {
				$query->className = $className;
				$sharded = $query->shard(null, $record);
				if (count($sharded) > 1 or $shard === '*') { // should be only one shard
					throw new Exception("Db_Mysql::insertManyAndExecute row should be stored on exactly one shard: " . Q::json_encode($record));
				}
				$shard = key($sharded);
			}
			
			// start filling out the query data
			$qc = empty($queryCounts[$shard]) ? 1 : $queryCounts[$shard] + 1;
			if (!isset($bindings[$shard])) {
				$bindings[$shard] = array();
			}
			$values_list = array();
			foreach ($record as $column => $value) {
				if ($value instanceof Db_Expression) {
					$values_list[] = "$value";
				} else {
					$values_list[] = ':_'.$qc.'_'.$column;
					$bindings[$shard]['_'.$qc.'_'.$column] = $value;
				}
			}
			$values_string = implode(', ', $values_list);
			if (empty($queryCounts[$shard])) {
				$q = $queries[$shard] = "INSERT INTO $into\nVALUES ($values_string) ";
				$queryCounts[$shard] = 1;
			} else {
				$q = $queries[$shard] .= ",\n       ($values_string) ";
				++$queryCounts[$shard];
			}

			// if chunk filled up for this shard, execute it
			if ($qc === $chunkSize) {
				if ($onDuplicateKeyUpdate) {
					$q .= $odku_clause;
				}
				$query = $this->rawQuery($q)->bind($bindings[$shard]);
				if ($onDuplicateKeyUpdate) {
					$query = $query->bind($update_fields);
				}
				if (isset($last_q[$shard]) and $last_q[$shard] === $q) {
					// re-use the prepared statement, save round-trips to the db
					$query->reuseStatement($last_queries[$shard]);
				}
				$query->execute(true, $shard);
				$last_q[$shard] = $q;
				$last_queries[$shard] = $query; // save for re-use
				$bindings[$shard] = $queries[$shard] = array();
				$queryCounts[$shard] = 0;
			}
		}
		
		// Now execute the remaining queries, if any
		foreach ($queries as $shard => $q) {
			if (!$q) continue;
			if ($onDuplicateKeyUpdate) {
				$q .= $odku_clause;
			}
			$query = $this->rawQuery($q)->bind($bindings[$shard]);
			if ($onDuplicateKeyUpdate) {
				$query = $query->bind($update_fields);
			}
			if (isset($last_q[$shard]) and $last_q[$shard] === $q) {
				// re-use the prepared statement, save round-trips to the db
				$query->reuseStatement($last_queries[$shard]);
			}
			$query->execute(true, $shard);
		}
		
		foreach ($rows as $row) {
			if ($row instanceof Db_Row) {
				$row->wasInserted(true);
				$row->wasRetrieved(true);
			}
		}
	}

	/**
	 * Creates a query to update rows. Needs to be used with {@link Db_Query::set}
	 * @method update
	 * @param {string} $table The table to update
	 * @return {Db_Query_Mysql} The resulting Db_Query object
	 */
	function update ($table)
	{		
		if (empty($table))
			throw new Exception("table not specified in call to 'update'.");
		
		$clauses = array('UPDATE' => "$table");
		return new Db_Query_Mysql($this, Db_Query::TYPE_UPDATE, $clauses, array(), $table);
	}

	/**
	 * Creates a query to delete rows.
	 * @method delete
	 * @param {string} $table_from The table to delete from
	 * @param {string} [$table_using=null] If set, adds a USING clause with this table. You can then use ->join() with the resulting Db_Query.
	 * @return {Db_Query_Mysql}
	 */
	function delete ($table_from, $table_using = null)
	{	
		if (empty($table_from))
			throw new Exception("table not specified in call to 'delete'.");

		if (isset($table_using) and !is_string($table_using)) {
			throw new Exception("table_using field must be a string");
		}

		if (isset($table_using))
			$clauses = array('FROM' => "$table_from USING $table_using");
		else
			$clauses = array('FROM' => "$table_from");
		return new Db_Query_Mysql($this, Db_Query::TYPE_DELETE, $clauses, array(), $table_from);
	}

	/**
	 * Creates a query from raw SQL
	 * @method rawQuery
	 * @param {string|null} $sql May contain one or more SQL statements.
	 *  Pass null here for an empty query that you can add other clauses to, e.g. ->commit().
	 * @param {array} [$bind=array()] An array of parameters to bind to the query, using
	 * the Db_Query_Mysql->bind method.
	 * @return {Db_Query_Mysql}
	 */
	function rawQuery ($sql = null, $bind = array())
	{
		$clauses = array('RAW' => $sql);
		$query = new Db_Query_Mysql($this, Db_Query::TYPE_RAW, $clauses);
		if ($bind) {
			$query->bind($bind);
		}
		return $query;
	}
	
	/**
	 * Creates a query to rollback a previously started transaction.
	 * @method update
	 * @param {array} $criteria The criteria to use, for sharding
	 * @return {Db_Query_Mysql} The resulting Db_Query object
	 */
	function rollback ($criteria = null)
	{
		$query = new Db_Query_Mysql($this, Db_Query::TYPE_ROLLBACK, array('ROLLBACK' => true));
		$query->rollback($criteria);
		return $query;
	}

    /**
     * Sorts a table in chunks
     * @method rank
     * @param {string} $table The name of the table in the database
     * @param {string} $pts_field The name of the field to rank by.
     * @param {string} $rank_field The rank field to update in all the rows
     * @param {integer} [$start=1] The value of the first rank
     * @param {integer} [$chunk_size=1000] The number of rows to process at a time. Default is 1000.
     * This is so the queries don't tie up the database server for very long,
     * letting it service website requests and other things.
     * @param {integer} [$rank_level2=0] Since the ranking is done in chunks, the function must know
     *  which rows have not been processed yet. If this field is empty (default)
     *  then the function sets the rank_field to 0 in all the rows, before
     *  starting the ranking process.
     *  (That might be a time consuming operation.)
     *  Otherwise, if $rank is a nonzero integer, then the function alternates
     *  between the ranges
     *  $start to $rank_level2, and $rank_level2 + $start to $rank_level2 * 2.
     *  That is, after it is finished, all the ratings will be in one of these
     *  two ranges.
     *  If not empty, this should be a very large number, like a billion.
     * @param {array} [$order_by] The order clause to use when calculating ranks.
     *  Default is array($pts_field, false)
     * @param {array} [$where=null] Any additional criteria to filter the table by.
	 *  The ranking algorithm will do its work within the results that match this criteria.
	 *  If your table is sharded, then all the work must be done within one shard.
     */
    function rank(
        $table,
        $pts_field, 
        $rank_field, 
		$start = 1,
        $chunk_size = 1000, 
        $rank_level2 = 0,
        $order_by = null,
		$where = array())
    {	
		if (!isset($order_by)) {
			$order_by = array($pts_field, false);
		}		
		if (!isset($where)) {
			$where = '1';
		}
        
        // Count all the rows
        $query = $this->select('COUNT(1) _count', $table)->where($where);
        $sharded = $query->shard();
	    $shard = key($sharded);
        if (count($sharded) > 1 or $shard === '*') { // should be only one shard
        	throw new Exception("Db_Mysql::rank can work within at most one shard");
        }
        $row = $query->execute()->fetch(PDO::FETCH_ASSOC);
        $count = $row['_count'];
		
        if (empty($rank_level2)) {
            $this->update($table)
                ->set(array($rank_field => 0))
				->where($where)
                ->execute();
            $rank_base = 0;
            $condition = "$rank_field = 0 OR $rank_field IS NULL";
        } else {
            $rows = $this->select($pts_field, $table)
                ->where("$rank_field < $rank_level2")
				->andWhere($where)
                ->limit(1)
                ->fetchAll();
            if (!empty($rows)) {
        		// There are no ranks above $rank_level2. Create ranks on level 2.
        		$rank_base = $rank_level2;
        		$condition = "$rank_field < $rank_level2";
        	} else {
        		// The ranks are all above $rank_level2. Create ranks on level 1.
        		$rank_base = 0;
        		$condition = "$rank_field >= $rank_level2";
        	}
        }
    	
        // Here comes the magic:
		$offset = 0;
		$rank_base += $start;
		$this->rawQuery("set @rank = $offset - 1")->execute(false, $shard);
        do {
			$query = $this->update($table)->set(array(
				$rank_field => new Db_Expression("$rank_base + (@rank := @rank + 1)")
			))->where($condition);
			if ($where) {
				$query = $query->andWhere($where);
			}
			if ($order_by) {
				$query = call_user_func_array(array($query, 'orderBy'), $order_by);
			}
			$query->limit($chunk_size)->execute();
			$offset += $chunk_size;
        } while ($count-$offset > 0);
    }

	/**
	 * Generate an ID that is unique in a table
	 * @method uniqueId
	 * @param {string} $table The name of the table
	 * @param {string} $field The name of the field to check for uniqueness.
	 *  You should probably have an index starting with this field.
	 * @param {array} [$where=array()] You can indicate conditions here to limit the search for
	 *  an existing value. The result is an id that is unique within a certain partition.
	 * @param {array} [$options=array()] Optional array used to override default options:
	 * @param {integer} [$options.length=8] The length of the ID to generate, after the prefix.
	 * @param {string} [$options.characters='abcdefghijklmnopqrstuvwxyz']  All the characters from which to construct the id
	 * @param {string} [$options.prefix=''] The prefix to prepend to the unique id.
	 * @param {callable} [$options.filter]
	 *     The name of a function that will take the generated string and
	 *     check it. The filter function can modify the string by returning another string,
	 *     or simply reject the string by returning false, in which another string will be
	 */
	function uniqueId(
		$table, 
		$field, 
		$where = array(),
		$options = array())
	{
		$length = 8;
		$characters = 'abcdefghijklmnopqrstuvwxyz';
		$prefix = '';
		extract($options);
		$count = strlen($characters);
		do {
			$id = $prefix;
			for ($i=0; $i<$length; ++$i) {
				$id .= $characters[mt_rand(0, $count-1)];
			}
			if (!empty($options['filter'])) {
				$ret = Q::call($options['filter'], array(compact('id', 'table', 'field', 'where', 'options')));
				if ($ret === false) {
					continue;
				} else if ($ret) {
					$id = $ret;
				}
			}
			$q = $this->select($field, $table)
				->where(array($field => $id));
			if ($where) {
				$q->andWhere($where);
			}
			$rows = $q->limit(1)->fetchAll();
		} while ($rows);
		return $id;
	}

	/**
	 * Returns a timestamp from a Date string
	 * @method fromDate
	 * @param {string} $datetime The Date string that comes from the db
	 * @return {integer} The timestamp
	 */
	function fromDate ($date)
	{
		$year = substr($date, 0, 4);
		$month = substr($date, 5, 2);
		$day = substr($date, 8, 2);

		return mktime(0, 0, 0, $month, $day, $year);
	}
    
	/**
	 * Returns a timestamp from a DateTime string
	 * @method fromDateTime
	 * @param {string} $datetime The DateTime string that comes from the db
	 * @return {integer} The timestamp
	 */
	function fromDateTime ($datetime)
	{
		if (is_numeric($datetime)) {
			return $datetime;
		}
		$year = substr($datetime, 0, 4);
		$month = substr($datetime, 5, 2);
		$day = substr($datetime, 8, 2);
		$hour = substr($datetime, 11, 2);
		$min = substr($datetime, 14, 2);
		$sec = substr($datetime, 17, 2);

		return mktime($hour, $min, $sec, $month, $day, $year);
	}

	/**
	 * Returns a Date string to store in the database
	 * @method toDate
	 * @param {string} $timestamp The UNIX timestamp, e.g. from a strtotime function
	 * @return {string}
	 */
	function toDate ($timestamp)
	{
		if (!is_numeric($timestamp)) {
			$timestamp = strtotime($timestamp);
		}
		if ($timestamp > 10000000000) {
			$timestamp = $timestamp / 1000;
		}
		return date('Y-m-d', $timestamp);
	}

	/**
	 * Returns a DateTime string to store in the database
	 * @method toDateTime
	 * @param {string} $timestamp The UNIX timestamp, e.g. from a strtotime function
	 * @return {string}
	 */
	function toDateTime ($timestamp)
	{
		if (!is_numeric($timestamp)) {
			$timestamp = strtotime($timestamp);
		}
		if ($timestamp > 10000000000) {
			$timestamp = $timestamp / 1000;
		}
		return date('Y-m-d H:i:s', $timestamp);
	}
	
	/**
	 * Returns the timestamp the db server would have, based on synchronization
	 * @method timestamp
	 * @return {integer}
	 */
	function getCurrentTimestamp()
	{
		static $dbtime = null, $phptime = null;
		if (!isset($dbtime)) {
			$phptime1 = time();
			$row = $this->select('CURRENT_TIMESTAMP', '')->execute()->fetch(PDO::FETCH_NUM);
			$dbtime = $this->fromDateTime($row[0]);
			$phptime2 = time();
			$phptime = round(($phptime1 + $phptime2) / 2);
		}
		return $dbtime + (time() - $phptime);
	}
	
	/**
	 * Takes a MySQL script and returns an array of queries.
	 * When DELIMITER is changed, respects that too.
	 * @method scriptToQueries
	 * @param {string} $script The text of the script
	 * @param {callable} [$callback=null] Optional callback to call for each query.
	 * @return {array} An array of the SQL queries.
	 */
	function scriptToQueries($script, $callback = null)
	{
		$this->reallyConnect();
		$version_string = $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
		$version_parts = explode('.', $version_string);
		sprintf("%1d%02d%02d", $version_parts[0], $version_parts[1], $version_parts[2]);
		
		$script_stripped = $script;
		return $this->scriptToQueries_internal($script_stripped, $callback);
	}
	/**
	 * Takes stripped MySQL script and returns an array of queries.
	 * When DELIMITER is changed, respects that too.
	 * @method scriptToQueries_internal
	 * @protected
	 * @param {string} $script The text of the script
	 * @param {callable} [$callback=null] Optional callback to call for each query.
	 * @return {array} An array of the SQL queries.
	 */	
	protected function scriptToQueries_internal($script, $callback = null)
	{
		$queries = array();
		
		$script_len = strlen($script);
	
		$this->reallyConnect();
		$version_string = $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
		$version_parts = explode('.', $version_string);
		$version = sprintf("%1d%02d%02d", $version_parts[0], $version_parts[1], $version_parts[2]);
		
		//$mode_n = 0;  // normal 
		$mode_c = 1;  // comments
		$mode_sq = 2; // single quotes
		$mode_dq = 3; // double quotes
		$mode_bt = 4; // backticks
		$mode_lc = 5; // line comment (hash or double-dash)
		$mode_ds = 6; // delimiter statement
		
		$cur_pos = 0;
		$d = ';'; // delimiter
		$d_len = strlen($d);
		$query_start_pos = 0;
		
		$del_start_pos_array = array();
		$del_end_pos_array = array();
		
		if (class_exists('Q_Config')) {
			$separator = Q_Config::expect('Db', 'sql', 'querySeparator');
		} else {
			$separator = "-------- NEXT QUERY STARTS HERE --------";
		}
		$found = strpos($script, $separator);
		if ($found !== false) {
			// This script was specially crafted for quick parsing
			$queries = explode($separator, $script);
			foreach ($queries as $i => $query) {
				if (!trim($query)) {
					unset($queries[$i]);
				}
			}
			return $queries;
		}
		
		while (1) {
			
			$c_pos = strpos($script, "/*", $cur_pos);
			$sq_pos = strpos($script, "'", $cur_pos);
			$dq_pos = strpos($script, "\"", $cur_pos);
			$bt_pos = strpos($script, "`", $cur_pos);
			$c2_pos = strpos($script, "--", $cur_pos);
			$c3_pos = strpos($script, "#", $cur_pos);
			$ds_pos = stripos($script, "\nDELIMITER ", $cur_pos);
			if ($cur_pos === 0 and substr($script, 0, 9) === 'DELIMITER') {
				$ds_pos = 0;
			}

			$next_pos = false;
			if ($c_pos !== false) {
				$next_mode = $mode_c;
				$next_pos = $c_pos;
				$next_end_str = "*/";
				$next_end_str_len = 2;
			}
			if ($sq_pos !== false and ($next_pos === false or $sq_pos < $next_pos)) {
				$next_mode = $mode_sq;
				$next_pos = $sq_pos;
				$next_end_str = "'";
				$next_end_str_len = 1;
			}
			if ($dq_pos !== false and ($next_pos === false or $dq_pos < $next_pos)) {
				$next_mode = $mode_dq;
				$next_pos = $dq_pos;
				$next_end_str = "\"";
				$next_end_str_len = 1;
			}
			if ($bt_pos !== false and ($next_pos === false or $bt_pos < $next_pos)) {
				$next_mode = $mode_bt;
				$next_pos = $bt_pos;
				$next_end_str = "`";
				$next_end_str_len = 1;
			}
			if ($c2_pos !== false and ($next_pos === false or $c2_pos < $next_pos)
			and ($script[$c2_pos+2] == " " or $script[$c2_pos+2] == "\t")) {
				$next_mode = $mode_lc;
				$next_pos = $c2_pos;
				$next_end_str = "\n";
				$next_end_str_len = 1;
			}
			if ($c3_pos !== false and ($next_pos === false or $c3_pos < $next_pos)) {
				$next_mode = $mode_lc;
				$next_pos = $c3_pos;
				$next_end_str = "\n";
				$next_end_str_len = 1;
			}
			if ($ds_pos !== false and ($next_pos === false or $ds_pos < $next_pos)) {
				$next_mode = $mode_ds;
				$next_pos = $ds_pos;
				$next_end_str = "\n";
				$next_end_str_len = 1;
			}
			
			// If at this point, $next_pos === false, then
			// we are in the final stretch.
			// Until the end of the string, we have normal mode.
			
			// Right now, we are in normal mode.
			$d_pos = strpos($script, $d, $cur_pos);
			while ($d_pos !== false and ($next_pos === false or $d_pos < $next_pos)) {
				$query = substr($script, $query_start_pos, $d_pos - $query_start_pos);
	
				// remove parts of the query string based on the "del_" arrays
				$del_pos_count = count($del_start_pos_array);
				if ($del_pos_count == 0) {
					$query2 = $query;
				} else {
					$query2 = substr($query, 0, $del_start_pos_array[0] - $query_start_pos);
					for ($i=1; $i < $del_pos_count; ++$i) {
						$query2 .= substr($query, $del_end_pos_array[$i-1]  - $query_start_pos, 
							$del_start_pos_array[$i] - $del_end_pos_array[$i-1]);
					}
					$query2 .= substr($query, 
						$del_end_pos_array[$del_pos_count - 1] - $query_start_pos);
				}
	
				$del_start_pos_array = array(); // reset these arrays
				$del_end_pos_array = array(); // reset these arrays
	
				$query_start_pos = $d_pos + $d_len;
				$cur_pos = $query_start_pos;
	
				$query2 = trim($query2);
				if ($query2)
					$queries[] = $query2; // <----- here is where we add to the main array
					if ($callback) {
						call_user_func($callback, $query2);
					}
				
				$d_pos = strpos($script, $d, $cur_pos);
			};
			
			if ($next_pos === false) {
				// Add the last query and get out of here:
				$query = substr($script, $query_start_pos);

				// remove parts of the query string based on the "del_" arrays
				$del_pos_count = count($del_start_pos_array);
				if ($del_pos_count == 0) {
					$query2 = $query;
				} else {
					$query2 = substr($query, 0, $del_start_pos_array[0] - $query_start_pos);
					for ($i=1; $i < $del_pos_count; ++$i) {
						$query2 .= substr($query, $del_end_pos_array[$i-1]  - $query_start_pos, 
							$del_start_pos_array[$i] - $del_end_pos_array[$i-1]);
					}
					if ($del_end_pos_array[$del_pos_count - 1] !== false) {
						$query2 .= substr($query, 
							$del_end_pos_array[$del_pos_count - 1] - $query_start_pos);
					}
				}
				
				$query2 = trim($query2);
				if ($query2) {
					$queries[] = $query2;
					if ($callback) {
						call_user_func($callback, $query2);
					}
				}
				break;
			}
			
			if ($next_mode == $mode_c) {
				// We are inside a comment
				$end_pos = strpos($script, $next_end_str, $next_pos + 1);
				if ($end_pos === false) {
					throw new Exception("unterminated comment -- missing terminating */ characters.");
				}
				
				$version_comment = false;
				if ($script[$next_pos + 2] == '!') {
					$ver = substr($script, $next_pos + 3, 5);
					if ($version >= $ver) {
						// we are in a version comment
						$version_comment = true;
					}
				}
				
				// Add to list of areas to ignore
				if ($version_comment) {
					$del_start_pos_array[] = $next_pos;
					$del_end_pos_array[] = $next_pos + 3 + 5;
					$del_start_pos_array[] = $end_pos;
					$del_end_pos_array[] = $end_pos + $next_end_str_len;
				} else {
					$del_start_pos_array[] = $next_pos;
					$del_end_pos_array[] = $end_pos + $next_end_str_len;
				}
			} else if ($next_mode == $mode_lc) {
				// We are inside a line comment
				$end_pos = strpos($script, $next_end_str, $next_pos + 1);
				$del_start_pos_array[] = $next_pos;
				if ($end_pos !== false) {
					$del_end_pos_array[] = $end_pos + $next_end_str_len;
				} else {
					$del_end_pos_array[] = false;
				}
			} else if ($next_mode == $mode_ds) {
				// We are inside a DELIMITER statement
				$start_pos = $next_pos;
				$end_pos = strpos($script, $next_end_str, $next_pos + 11);
				$del_start_pos_array[] = $next_pos;
				if ($end_pos !== false) {
					$del_end_pos_array[] = $end_pos + $next_end_str_len;
				} else {
					// this is the last statement in the script, it seems.
					// Might look funny, like:
					// DELIMITER aa sfghjkhsgkjlfhdsgjkfdglhdfsgkjfhgjdlk
					$del_end_pos_array[] = false;
				}
				// set a new delimiter!
				$try_d = trim(substr($script, $ds_pos + 11, $end_pos - ($ds_pos + 11)));
				if (!empty($try_d)) {
					$d = $try_d;
					$d_len = strlen($d);
				} // otherwise malformed delimiter statement or end of file
			} else {
				// We are inside a string
				$start_pos = $next_pos;
				$try_end_pos = $next_pos;
				do {
					$end_pos = false;
					$try_end_pos = strpos($script, $next_end_str, $try_end_pos + 1);
					if ($try_end_pos === false) {
						throw new Exception("unterminated string -- missing terminating $next_end_str character.");
					}
					if ($try_end_pos+1 >= $script_len) {
						$end_pos = $try_end_pos;
						break;
					}
					if ($script[$try_end_pos+1] == $next_end_str) {
						++$try_end_pos;
						continue;
					}
					$bs_count = 0;
					for ($i = $try_end_pos - 1; $i > $next_pos; --$i) {
						if ($script[$i] == "\\") {
							++$bs_count;
						} else {
							break;
						}
					}
					if ($bs_count % 2 == 0) {
						$end_pos = $try_end_pos;
					}
				} while ($end_pos === false);
				// If we are here, we have found the end of the string,
				// and are back in normal mode.
			}
	
			// We have exited the previous mode and set end_pos.
			if ($end_pos === false)
				break;
			$cur_pos = $end_pos + $next_end_str_len;
		}
		
		foreach ($queries as $i => $query) {
			if ($query === false) {
				unset($queries[$i]);
			}
		}

		return $queries;
	}
	
	/**
	 * Generates base classes of the models, and if they don't exist,
	 * skeleton code for the models themselves. 
	 * Use it only after you have made changes to the database schema.
	 * You shouldn't be using it on every request.
	 * @method generateModels
	 * @param {string} $directory The directory in which to generate the files.
	 *  If the files already exist, they are not overwritten,
	 *  unless they are inside the "Base" subdirectory.
	 *  If the "Base" subdirectory does not exist, it is created.
	 * @param {string} [$classname_prefix=null] The prefix to prepend to the Base class names.
	 *  If not specified, prefix becomes "connectionName_",  where connectionName is the name of the connection.
	 * @return {array} $filenames The array of filenames for files that were saved.
	 * @throws {Exception} If the $connection is not registered, or the $directory
	 *  does not exist, this function throws an exception.
	 */
	function generateModels (
		$directory, 
		$classname_prefix = null)
	{
		$dc = '/**';
		if (!file_exists($directory))
			throw new Exception("directory $directory does not exist.");
		
		$connectionName = $this->connectionName();
		$conn = Db::getConnection($connectionName);
		
		$prefix = empty($conn['prefix']) ? '' : $conn['prefix'];
		$prefix_len = strlen($prefix);
		
		if (!isset($classname_prefix)) {
			$classname_prefix = isset($connectionName) ? $connectionName . '_' : '';
		}
		
		$rows = $this->rawQuery('SHOW TABLES')->fetchAll();
		
		if (class_exists('Q_Config')) {
			$ext = Q_Config::get('Q', 'extensions', 'class', 'php');
		} else {
			$ext = 'php';
		}
			
		$table_classnames = array();
		$js_table_classes_string = '';
		$class_name_prefix = rtrim(ucfirst($classname_prefix), "._");
		
		$filenames = array();

		foreach ($rows as $row) {
			$table_name = $row[0];
			$table_name_base = substr($table_name, $prefix_len);
			$table_name_prefix = substr($table_name, 0, $prefix_len);
			if (empty($table_name_base) or $table_name_prefix != $prefix
			or stristr($table_name, '_Q_') !== false) {
				continue; // no class generated
			}
			
			$class_name_base = null;
			$js_base_class_string = '';
			$base_class_string = $this->codeForModelBaseClass(
				$table_name, 
				$directory, 
				$classname_prefix, 
				$class_name_base, 
				null,
				$js_base_class_string,
				$table_comment
			); // sets the $class_name variable
			$class_name = ucfirst($classname_prefix) . $class_name_base;
			if (empty($class_name)) {
				continue; // no class generated
			}
	
			$class_name_parts = explode('_', $class_name);
			$class_filename = $directory.DS.implode(DS, $class_name_parts).'.php';
			$base_class_filename = $directory.DS.'Base'.DS.implode(DS, $class_name_parts).'.php';
			$js_class_filename = $directory.DS.implode(DS, $class_name_parts).'.js';
			$js_base_class_filename = $directory.DS.'Base'.DS.implode(DS, $class_name_parts).'.js';
			$js_base_class_require = 'Base/'.implode('/', $class_name_parts);
			$js_class_name = implode('.', $class_name_parts);
			$js_base_class_name = implode('.Base.', $class_name_parts);

			$class_extras = is_readable($class_filename.'.inc') ? file_get_contents($class_filename.'.inc') : '';
			$js_class_extras = is_readable($js_class_filename.'.inc') ? file_get_contents($js_class_filename.'.inc') : '';
			
			if ($class_extras) {
				$class_extras = <<<EOT
					
	/* * * *
	 * Including content of '$class_name_base.php.inc' below
	 * * * */
$class_extras
	/* * * */
	
EOT;
			}
			if ($js_class_extras) {
				$js_class_extras = <<<EOT
					
	/* * * *
	 * Including content of '$class_name_base.js.inc' below
	 * * * */
$class_extras
	/* * * */
	
EOT;
			}

			$class_string = <<<EOT
<?php
$dc
 * @module $connectionName
 */
$dc
 * Class representing '$class_name_base' rows in the '$connectionName' database
 * You can create an object of this class either to
 * access its non-static methods, or to actually
 * represent a $table_name_base row in the $connectionName database.
 *
 * @class $class_name
 * @extends Base_$class_name
 */
class $class_name extends Base_$class_name
{
	$dc
	 * The setUp() method is called the first time
	 * an object of this class is constructed.
	 * @method setUp
	 */
	function setUp()
	{
		parent::setUp();
		// INSERT YOUR CODE HERE
		// e.g. \$this->hasMany(...) and stuff like that.
	}

	/*
	 * Add any $class_name methods here, whether public or not
	 */
	 
	$dc
	 * Implements the __set_state method, so it can work with
	 * with var_export and be re-imported successfully.
	 * @method __set_state
	 * @static
	 * @param {array} \$array
	 * @return {{$class_name}} Class instance
	 */
	static function __set_state(array \$array) {
		\$result = new $class_name();
		foreach(\$array as \$k => \$v)
			\$result->\$k = \$v;
		return \$result;
	}
};
EOT;

			$js_class_string = <<<EOT
$dc
 * Class representing $table_name_base rows.
 *
 * This description should be revised and expanded.
 *
 * @module $connectionName
 */
var Q = require('Q');
var Db = Q.require('Db');
var $class_name_base = Q.require('$js_base_class_require');

$dc
 * Class representing '$class_name_base' rows in the '$connectionName' database
$table_comment * @namespace $class_name_prefix
 * @class $class_name_base
 * @extends Base.$js_class_name
 * @constructor
 * @param {Object} fields The fields values to initialize table row as
 * an associative array of {column: value} pairs
 */
function $class_name (fields) {

	// Run mixed-in constructors
	$class_name.constructors.apply(this, arguments);
	
	/*
 	 * Add any privileged methods to the model class here.
	 * Public methods should probably be added further below.
	 */
}

Q.mixin($class_name, $class_name_base);

/*
 * Add any public methods here by assigning them to $class_name.prototype
 */

$dc
 * The setUp() method is called the first time
 * an object of this class is constructed.
 * @method setUp
 */
$class_name.prototype.setUp = function () {
	// put any code here
	// overrides the Base class
};

module.exports = $class_name;
EOT;

			// overwrite base class file if necessary, but not the class file
			Db_Utils::saveTextFile($base_class_filename, $base_class_string);
			$filenames[] = $base_class_filename;
			Db_Utils::saveTextFile($js_base_class_filename, $js_base_class_string);
			$filenames[] = $js_base_class_filename;
			if (! file_exists($class_filename)) {
				Db_Utils::saveTextFile($class_filename, $class_string);
				$filenames[] = $class_filename;
			}
			if (! file_exists($js_class_filename)) {
				Db_Utils::saveTextFile($js_class_filename, $js_class_string);
				$filenames[] = $js_class_filename;
			}
			
			$table_classnames[] = $class_name;
			$js_table_classes_string .= <<<EOT

$dc
 * Link to $connectionName.$class_name_base model
 * @property $class_name_base
 * @type $connectionName.$class_name_base
 */
Base.$class_name_base = Q.require('$connectionName/$class_name_base');

EOT;
		}
		
		// Generate the "module model" base class file
		$table_classnames_exported = var_export($table_classnames, true);
		$table_classnames_json = $pk_json_indented = str_replace(
			array("[", ",", "]"),
			array("[\n\t", ",\n\t", "\n]"),
			json_encode($table_classnames)
		);
		if (!empty($connectionName)) {
			$class_name = Db::generateTableClassName($connectionName);
			$class_name_parts = explode('_', $class_name);
			$class_filename = $directory.DS.implode(DS, $class_name_parts).'.php';
			$base_class_filename = $directory.DS.'Base'.DS.implode(DS, $class_name_parts).'.php';
			$js_class_filename = $directory.DS.implode(DS, $class_name_parts).'.js';
			$js_base_class_filename = $directory.DS.'Base'.DS.implode(DS, $class_name_parts).'.js';
			$js_base_class_require = 'Base'.'/'.implode('/', $class_name_parts);
			$dbname =
			// because table name can be {$prefix}_Q_plugin or {$prefix}_Q_app we need to know correct table name
			$tables = $this->rawQuery(
				"SELECT table_comment"
				." FROM INFORMATION_SCHEMA.TABLES"
				." WHERE table_schema = '{$this->dbname}' and table_name LIKE '{$prefix}Q_%'"
			)->execute()->fetchAll(PDO::FETCH_NUM);
			$model_comment = (!empty($tables[0]['table_comment']))
				 ? " * <br>{".$tables[0]['table_comment']."}\n"
				 : '';
			$model_extras = is_readable($class_filename.'.inc') ? file_get_contents($class_filename.'.inc') : '';
			$js_model_extras = is_readable($js_class_filename.'.inc') ? file_get_contents($js_class_filename.'.inc') : '';

			$base_class_string = <<<EOT
<?php

$dc
 * Autogenerated base class for the $connectionName model.
 * 
 * Don't change this file, since it can be overwritten.
 * Instead, change the $class_name.php file.
 *
 * @module $connectionName
 */
$dc
 * Base class for the $class_name model
 * @class Base_$class_name
 */
abstract class Base_$class_name
{
	$dc
	 * The list of model classes
	 * @property \$table_classnames
	 * @type array
	 */
	static \$table_classnames = $table_classnames_exported;
$class_extras
	$dc
     * This method calls Db.connect() using information stored in the configuration.
     * If this has already been called, then the same db object is returned.
	 * @method db
	 * @return {Db_Interface} The database object
	 */
	static function db()
	{
		return Db::connect('$connectionName');
	}

	$dc
	 * The connection name for the class
	 * @method connectionName
	 * @return {string} The name of the connection
	 */
	static function connectionName()
	{
		return '$connectionName';
	}
};
EOT;

			$js_base_class_string = <<<EOT
$dc
 * Autogenerated base class for the $connectionName model.
 * 
 * Don't change this file, since it can be overwritten.
 * Instead, change the $class_name.js file.
 *
 * @module $connectionName
 */
var Q = require('Q');
var Db = Q.require('Db');
$js_class_extras
$dc
 * Base class for the $class_name model
 * @namespace Base
 * @class $class_name
 * @static
 */
function Base () {
	return this;
}
 
module.exports = Base;

$dc
 * The list of model classes
 * @property tableClasses
 * @type array
 */
Base.tableClasses = $table_classnames_json;

$dc
 * This method calls Db.connect() using information stored in the configuration.
 * If this has already been called, then the same db object is returned.
 * @method db
 * @return {Db} The database connection
 */
Base.db = function () {
	return Db.connect('$connectionName');
};

$dc
 * The connection name for the class
 * @method connectionName
 * @return {string} The name of the connection
 */
Base.connectionName = function() {
	return '$connectionName';
};
$js_table_classes_string
EOT;

		$class_string = <<<EOT
<?php
$dc
 * $class_name_prefix model
$model_comment * @module $connectionName
 * @main $connectionName
 */
$dc
 * Static methods for the $connectionName models.
 * @class $class_name
 * @extends Base_$class_name
 */
abstract class $class_name extends Base_$class_name
{
	/*
	 * This is where you would place all the static methods for the models,
	 * the ones that don't strongly pertain to a particular row or table.
	 * If file '$class_name.php.inc' exists, its content is included
	 * * * */
$model_extras
	/* * * */
};
EOT;

		$js_class_string = <<<EOT
$dc
 * $class_name_prefix model
$model_comment * @module $connectionName
 * @main $connectionName
 */
var Q = require('Q');

$dc
 * Static methods for the $class_name_prefix model
 * @class $class_name_prefix
 * @extends Base.$class_name_prefix
 * @static
 */
function $connectionName() { };
module.exports = $connectionName;

var Base_$connectionName = Q.require('$js_base_class_require');
Q.mixin($connectionName, Base_$connectionName);

/*
 * This is where you would place all the static methods for the models,
 * the ones that don't strongly pertain to a particular row or table.
 * Just assign them as methods of the $connectionName object.
 * If file '$class_name.js.inc' exists, its content is included
 * * * */
$js_model_extras
/* * * */
EOT;

			// overwrite base class file if necessary, but not the class file
			Db_Utils::saveTextFile($base_class_filename, $base_class_string);
			$filenames[] = $base_class_filename;
			Db_Utils::saveTextFile($js_base_class_filename, $js_base_class_string);
			$filenames[] = $js_base_class_filename;
			if (! file_exists($class_filename)) {
				$filenames[] = $class_filename;
				Db_Utils::saveTextFile($class_filename, $class_string);
			}
			if (! file_exists($js_class_filename)) {
				$filenames[] = $js_class_filename;
				Db_Utils::saveTextFile($js_class_filename, $js_class_string);
			}
		}
		$directoryLen = strlen($directory.DS);
		foreach ($filenames as $i => $filename) {
			$filenames[$i] = substr($filename, $directoryLen);
		}
		return $filenames;
	}

	/**
	 * Generates code for a base class for the model
	 * @method codeForModelBaseClass
	 * @param {string} $table The name of the table to generate the code for.
	 * @param {string} $directory The path of the directory in which to place the model code.
	 * @param {string} [$classname_prefix=''] The prefix to prepend to the generated class names
	 * @param {&string} [$class_name_base=null] If set, this is the class name that is used.
	 *  If an unset variable is passed, it is filled with the
	 *  class name that is ultimately chosen, without the $classname_prefix
	 * @param {string} [$prefix=null] Defaults to the prefix of the tables, as specified in the connection.
	 *  Pass null here to use the default, or a string to override it.
	 * @param {&string} [$js_code=null] The javascript code for the base class
	 * @param {&string} [$table_comment=''] The comment from the MySQL table if any
	 * @return {string} The generated code for the class.
	 */
	function codeForModelBaseClass (
		$table_name, 
		$directory,
		$classname_prefix = '',
		&$class_name_base = null,
		$prefix = null,
		&$js_code = null,
		&$table_comment = '')
	{
		$dc = '/**';
		if (empty($table_name))
			throw new Exception('table_name parameter is empty', - 2);
		if (empty($directory))
			throw new Exception('directory parameter is empty', - 3);
	
		$connectionName = $this->connectionName();
		$conn = Db::getConnection($connectionName);
		
		if (!isset($prefix)) {
			$prefix = empty($conn['prefix']) ? '' : $conn['prefix'];
		}
		if (!empty($prefix)) {
			$prefix_len = strlen($prefix);
			$table_name_base = substr($table_name, $prefix_len);
			$table_name_prefix = substr($table_name, 0, $prefix_len);
			if (empty($table_name_base) or $table_name_prefix != $prefix)
				return ''; // no class generated
		} else {
			$table_name_base = $table_name;
		}

		if (empty($classname_prefix))
			$classname_prefix = '';
		if (!isset($class_name_base)) {
			$class_name_base = Db::generateTableClassName($table_name_base);
		}
		$class_name = ucfirst($classname_prefix) . $class_name_base;
		$table_cols = $this->rawQuery("SHOW FULL COLUMNS FROM $table_name")->execute()->fetchAll(PDO::FETCH_ASSOC);
		$table_status = $this->rawQuery("SHOW TABLE STATUS WHERE Name = '$table_name'")->execute()->fetchAll(PDO::FETCH_COLUMN, 17);
		$table_comment = (!empty($table_status[0])) ? " * <br>{$table_status[0]}\n" : '';
		// Calculate primary key
		$pk = array();
		foreach ($table_cols as $table_col) {
			if ($table_col['Key'] == 'PRI') {
				$pk[] = $table_col['Field'];
			}
		}
		$pk_exported = var_export($pk, true);
		$pk_json = json_encode($pk);
		
		// Calculate validation functions
		$functions = array();
		$js_functions = array();
		$field_names = array();
		$properties = array();
		$js_properties = array();
		$required_field_names = array();
		$magic_field_names = array();
		$defaults = array();
		$comments = array();
		foreach ($table_cols as $table_col) {
			$is_magic_field = null;
			$field_name = $table_col['Field'];
			$field_names[] = $field_name;
			$field_null = $table_col['Null'] == 'YES' ? true : false;
			$field_nulls[] = $field_null;
			$field_default = $table_col['Default'];
			$comments[] = $table_col['Comment'];
			$field_name_safe = preg_replace('/[^0-9a-zA-Z\_]/', '_', $field_name);
			$auto_inc = (strpos($table_col['Extra'], 'auto_increment') !== false);
			$type = $table_col['Type'];
			$pieces = explode('(', $type);
			if (isset($pieces[1])) {
				$pieces2 = explode(')', $pieces[1]);
				$pieces2_count = count($pieces2);
				if ($pieces2_count > 2) { // could happen if enum's values have ")"
					$pieces2 = array(
						implode(')', array_slice($pieces2, 0, -1)), 
						end($pieces2)
					);
				}
			}
			$type_name = $pieces[0];
			if (isset($pieces2)) {
				$type_display_range = $pieces2[0];
				$type_modifiers = $pieces2[1];
				$type_unsigned = (strpos($type_modifiers, 'unsigned') !== false);
			}
			
			$isTextLike = false;
			$isNumberLike = false;
			$isTimeLike = false;
			
			switch ($type_name) {
				case 'tinyint':
					$type_range_min = $type_unsigned ? 0 : - 128;
					$type_range_max = $type_unsigned ? 255 : 127;
					break;
				case 'smallint':
					$type_range_min = $type_unsigned ? 0 : - 32768;
					$type_range_max = $type_unsigned ? 65535 : 32767;
					break;
				case 'mediumint':
					$type_range_min = $type_unsigned ? 0 : - 8388608;
					$type_range_max = $type_unsigned ? 16777215 : 8388607;
					break;
				case 'int':
					$type_range_min = $type_unsigned ? 0 : - 2147483648;
					$type_range_max = $type_unsigned ? 4294967295 : 2147483647;
					break;
				case 'bigint':
					$type_range_min = $type_unsigned ? 0 : - 9223372036854775808;
					$type_range_max = $type_unsigned ? 18446744073709551615 : 9223372036854775807;
					break;
				case 'tinytext':
				case 'tinyblob':
					$type_display_range = 255;
					break;
				case 'text':
				case 'blob':
					$type_display_range = 65535;
					break;
				case 'mediumtext':
				case 'mediumblob':
					$type_display_range = 16777216;
					break;
				case 'longtext':
				case 'longblob':
					$type_display_range = 4294967296;
					break;
			}
			$field_name_exported = var_export($field_name, true);
			
			$null_check = $field_null ? "if (!isset(\$value)) {\n\t\t\treturn array($field_name_exported, \$value);\n\t\t}\n\t\t" : '';
			$null_fix = $field_null ? '' : "if (!isset(\$value)) {\n\t\t\t\$value='';\n\t\t}\n\t\t";
			$dbe_check = "if (\$value instanceof Db_Expression) {\n\t\t\treturn array($field_name_exported, \$value);\n\t\t}\n\t\t";
			$js_null_check = $field_null ? "if (value == undefined) return value;\n\t\t" : '';
			$js_null_fix = $field_null ? '' : "if (value == null) {\n\t\t\tvalue='';\n\t\t}\n\t\t";
			$js_dbe_check = "if (value instanceof Db.Expression) return value;\n\t\t";
			if (! isset($functions["beforeSet_$field_name_safe"]))
				$functions["beforeSet_$field_name_safe"] = array();
			if (! isset($js_functions["beforeSet_$field_name_safe"]))
				$js_functions["beforeSet_$field_name_safe"] = array();
			$type_name_lower = strtolower($type_name);
			switch ($type_name_lower) {
				case 'tinyint':
				case 'smallint':
				case 'int':
				case 'mediumint':
				case 'bigint':
					$isNumberLike = true;
					$properties[]="integer $field_name";
					$js_properties[] = "Integer $field_name";
					$functions["maxSize_$field_name_safe"]['comment'] = <<<EOT
	$dc
	 * @method maxSize_$field_name_safe
	 * Returns the maximum integer that can be assigned to the $field_name field
	 * @return {integer}
	 */
EOT;
					$functions["maxSize_$field_name_safe"]['args'] = '';
					$functions["maxSize_$field_name_safe"]['return_statement'] = <<<EOT
		return $type_range_max;
EOT;
					$functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$null_check}{$dbe_check}if (!is_numeric(\$value) or floor(\$value) != \$value)
			throw new Exception('Non-integer value being assigned to '.\$this->getTable().".$field_name");
		\$value = intval(\$value);
		if (\$value < $type_range_min or \$value > $type_range_max) {
			\$json = json_encode(\$value);
			throw new Exception("Out-of-range value \$json being assigned to ".\$this->getTable().".$field_name");
		}
EOT;
					$functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
	$dc
	 * Method is called before setting the field and verifies if integer value falls within allowed limits
	 * @method beforeSet_$field_name_safe
	 * @param {integer} \$value
	 * @return {array} An array of field name and value
	 * @throws {Exception} An exception is thrown if \$value is not integer or does not fit in allowed range
	 */
EOT;
					$js_functions["maxSize_$field_name_safe"]['comment'] = <<<EOT
$dc
 * Returns the maximum integer that can be assigned to the $field_name field
 * @return {integer}
 */
EOT;
					$js_functions["maxSize_$field_name_safe"]['args'] = '';
					$js_functions["maxSize_$field_name_safe"]['return_statement'] = <<<EOT
		return $type_range_max;
EOT;
					$js_functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$js_null_check}{$js_dbe_check}value = Number(value);
		if (isNaN(value) || Math.floor(value) != value) 
			throw new Error('Non-integer value being assigned to '+this.table()+".$field_name");
		if (value < $type_range_min || value > $type_range_max)
			throw new Error("Out-of-range value "+JSON.stringify(value)+" being assigned to "+this.table()+".$field_name");
EOT;
					$js_functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
$dc
 * Method is called before setting the field and verifies if integer value falls within allowed limits
 * @method beforeSet_$field_name_safe
 * @param {integer} value
 * @return {integer} The value
 * @throws {Error} An exception is thrown if 'value' is not integer or does not fit in allowed range
 */
EOT;
					$default = isset($table_col['Default'])
						? $table_col['Default']
						: ($field_null ? null : 0);
					$defaults[] = json_encode((int)$default);
					break;

				case 'enum':
					$properties[]="string $field_name";
					$js_properties[] = "String $field_name";
					$functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$null_check}{$dbe_check}if (!in_array(\$value, array($type_display_range)))
			throw new Exception("Out-of-range value '\$value' being assigned to ".\$this->getTable().".$field_name");
EOT;
					$functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
	$dc
	 * Method is called before setting the field and verifies if value belongs to enum values list
	 * @method beforeSet_$field_name_safe
	 * @param {string} \$value
	 * @return {array} An array of field name and value
	 * @throws {Exception} An exception is thrown if \$value does not belong to enum values list
	 */
EOT;
					$js_functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$js_null_check}{$js_dbe_check}if ([$type_display_range].indexOf(value) < 0)
			throw new Error("Out-of-range value "+JSON.stringify(value)+" being assigned to "+this.table()+".$field_name");
EOT;
					$js_functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
$dc
 * Method is called before setting the field and verifies if value belongs to enum values list
 * @method beforeSet_$field_name_safe
 * @param {string} value
 * @return {string} The value
 * @throws {Error} An exception is thrown if 'value' does not belong to enum values list
 */
EOT;
					$default = isset($table_col['Default'])
						? $table_col['Default']
						: null;
					$defaults[] = json_encode($default);
					break;
				
				case 'char':
				case 'varchar':
				case 'binary':
				case 'varbinary':
				case 'tinytext':
				case 'text':
				case 'mediumtext':
				case 'longtext':
				case 'tinyblob':
				case 'blob':
				case 'mediumblob':
				case 'longblob':
					$isTextLike = true;
					$isBinary = in_array($type_name_lower, array(
						'binary', 'varbinary',
						'tinyblob', 'blob', 'mediumblob', 'longblob'
					));
					$orBuffer1 = $isBinary ? "|Buffer" : "";
					$properties[]="string $field_name";
					$js_properties[] = "String$orBuffer1 $field_name";
					$functions["maxSize_$field_name_safe"]['comment'] = <<<EOT
	$dc
	 * Returns the maximum string length that can be assigned to the $field_name field
	 * @return {integer}
	 */
EOT;
					$functions["maxSize_$field_name_safe"]['args'] = '';
					$functions["maxSize_$field_name_safe"]['return_statement'] = <<<EOT
		return $type_display_range;
EOT;
					$functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$null_check}{$null_fix}{$dbe_check}if (!is_string(\$value) and !is_numeric(\$value))
			throw new Exception('Must pass a string to '.\$this->getTable().".$field_name");

EOT;
					if ($type_display_range and $type_display_range < $this->maxCheckStrlen) {
						$functions["beforeSet_$field_name_safe"][] = <<<EOT
		if (strlen(\$value) > $type_display_range)
			throw new Exception('Exceedingly long value being assigned to '.\$this->getTable().".$field_name");
EOT;
					}
					$functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
	$dc
	 * Method is called before setting the field and verifies if value is string of length within acceptable limit.
	 * Optionally accept numeric value which is converted to string
	 * @method beforeSet_$field_name
	 * @param {string} \$value
	 * @return {array} An array of field name and value
	 * @throws {Exception} An exception is thrown if \$value is not string or is exceedingly long
	 */
EOT;
					$js_functions["maxSize_$field_name_safe"]['comment'] = <<<EOT
	$dc
	 * Returns the maximum string length that can be assigned to the $field_name field
	 * @return {integer}
	 */
EOT;
					$js_functions["maxSize_$field_name_safe"]['args'] = '';
					$js_functions["maxSize_$field_name_safe"]['return_statement'] = <<<EOT
		return $type_display_range;
EOT;
					$bufferCheck = $isBinary ? " && !(value instanceof Buffer)" : "";
					$orBuffer2 = $isBinary ? " or Buffer" : "";
					$js_functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$js_null_check}{$js_null_fix}{$js_dbe_check}if (typeof value !== "string" && typeof value !== "number"$bufferCheck)
			throw new Error('Must pass a String$orBuffer2 to '+this.table()+".$field_name");
		if (typeof value === "string" && value.length > $type_display_range)
			throw new Error('Exceedingly long value being assigned to '+this.table()+".$field_name");
EOT;
					$js_functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
$dc
 * Method is called before setting the field and verifies if value is string of length within acceptable limit.
 * Optionally accept numeric value which is converted to string
 * @method beforeSet_$field_name_safe
 * @param {string} value
 * @return {string} The value
 * @throws {Error} An exception is thrown if 'value' is not string or is exceedingly long
 */
EOT;
					$default = isset($table_col['Default'])
						? $table_col['Default']
						: ($field_null ? null : '');
					$defaults[] = json_encode($default);
					break;
				
				case 'date':
					$isTimeLike = true;
					$properties[]="string|Db_Expression $field_name";
					$js_properties[] = "String|Db.Expression $field_name";
					$functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$null_check}{$dbe_check}\$date = date_parse(\$value);
		if (!empty(\$date['errors'])) {
			\$json = json_encode(\$value);
			throw new Exception("Date \$json in incorrect format being assigned to ".\$this->getTable().".$field_name");
		}
		\$value = date("Y-m-d H:i:s", strtotime(\$value));
		\$date = date_parse(\$value);
		foreach (array('year', 'month', 'day', 'hour', 'minute', 'second') as \$v) {
			\$\$v = \$date[\$v];
		}
		\$value = sprintf("%04d-%02d-%02d", \$year, \$month, \$day);
EOT;
					$functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
	$dc
	 * Method is called before setting the field and normalize the date string
	 * @method beforeSet_$field_name_safe
	 * @param {string} \$value
	 * @return {array} An array of field name and value
	 * @throws {Exception} An exception is thrown if \$value does not represent valid date
	 */
EOT;
					$js_functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$js_null_check}{$js_dbe_check}value = (value instanceof Date) ? Base.db().toDateTime(value) : value;
EOT;
					$js_functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
$dc
 * Method is called before setting the field
 * @method beforeSet_$field_name_safe
 * @param {String} value
 * @return {Date|Db.Expression} If 'value' is not Db.Expression the current date is returned
 */
EOT;
					$default = isset($table_col['Default'])
						? $table_col['Default']
						: ($field_null ? null : '');
					if ($default === 'CURRENT_TIMESTAMP'
					or strpos($default, '(') !== false) {
						$default = "new Db_Expression($default)";
					}
					$isExpression = (
						$default === 'CURRENT_TIMESTAMP'
						or strpos($default, '(') !== false
					);
					$defaults[] = $isExpression
						? 'new Db_Expression(' . json_encode($default) . ')'
						: json_encode($default);
					break;
				case 'datetime':
				case 'timestamp':
					$isTimeLike = true;
					$properties[]="string|Db_Expression $field_name";
					$js_properties[] = "String|Db.Expression $field_name";
					$possibleMagicFields = array('insertedTime', 'updatedTime', 'created_time', 'updated_time');
					$possibleMagicInsertFields = array('insertedTime', 'created_time');
					if (in_array($field_name, $possibleMagicFields)) {
						if (!in_array($field_name, $possibleMagicInsertFields)
						or !isset($field_default)) {
							$magic_field_names[] = $field_name;
							$is_magic_field = true;
						}
					}
					$functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$null_check}{$dbe_check}if (\$value instanceof DateTime) {
			\$value = \$value->getTimestamp();
		}
		if (is_numeric(\$value)) {
			\$newDateTime = new DateTime();
			\$datetime = \$newDateTime->setTimestamp(\$value);
		} else {
			\$datetime = new DateTime(\$value);
		}
		\$value = \$datetime->format("Y-m-d H:i:s");
EOT;
					$functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
	$dc
	 * Method is called before setting the field and normalize the DateTime string
	 * @method beforeSet_$field_name_safe
	 * @param {string} \$value
	 * @return {array} An array of field name and value
	 * @throws {Exception} An exception is thrown if \$value does not represent valid DateTime
	 */
EOT;
					$js_functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$js_null_check}{$js_dbe_check}if (typeof value !== 'object' && !isNaN(value)) {
			value = parseInt(value);
			value = new Date(value < 10000000000 ? value * 1000 : value);
		}
		value = (value instanceof Date) ? Base.db().toDateTime(value) : value;
EOT;
					$js_functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
$dc
 * Method is called before setting the field
 * @method beforeSet_$field_name_safe
 * @param {String} value
 * @return {Date|Db.Expression} If 'value' is not Db.Expression the current date is returned
 */
EOT;
					$default = isset($table_col['Default'])
						? $table_col['Default']
						: ($field_null ? null : '');
					$isExpression = (
						$default === 'CURRENT_TIMESTAMP'
						or strpos($default, '(') !== false
					);
					$defaults[] = $isExpression
						? 'new Db_Expression(' . json_encode($default) . ')'
						: json_encode($default);
					break;

				case 'numeric':
				case 'decimal':
				case 'float':
				case 'double':
					$isNumberLike = true;
					$properties[]="float $field_name";
					$js_properties[] = "Number $field_name";
					$functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$null_check}{$dbe_check}if (!is_numeric(\$value))
			throw new Exception('Non-numeric value being assigned to '.\$this->getTable().".$field_name");
		\$value = floatval(\$value);
EOT;
					$js_functions["beforeSet_$field_name_safe"][] = <<<EOT
		{$js_null_check}{$js_dbe_check}value = Number(value);
		if (isNaN(value))
			throw new Error('Non-number value being assigned to '+this.table()+".$field_name");
EOT;
					$js_functions["beforeSet_$field_name_safe"]['comment'] = <<<EOT
$dc
 * Method is called before setting the field to verify if value is a number
 * @method beforeSet_$field_name_safe
 * @param {integer} value
 * @return {integer} The value
 * @throws {Error} If 'value' is not number
 */
EOT;
					$default = isset($table_col['Default'])
						? $table_col['Default']
						: ($field_null ? null : 0);
					$defaults[] = json_encode((double)$default);
					break;

				default:
					$properties[]="mixed $field_name";
					$js_properties[] = "mixed $field_name";
					$default = isset($table_col['Default'])
						? $table_col['Default']
						: ($field_null ? null : '');
					$defaults[] = json_encode($default);
					break;
			}
			if (! empty($functions["beforeSet_$field_name_safe"])) {
				$functions["beforeSet_$field_name_safe"]['return_statement'] = <<<EOT
		return array('$field_name', \$value);
EOT;
			}
			if (! empty($js_functions["beforeSet_$field_name_safe"])) {
				$js_functions["beforeSet_$field_name_safe"]['return_statement'] = <<<EOT
		return value;
EOT;
			}
			if (! $field_null and ! $is_magic_field
			and ((!$isNumberLike and !$isTextLike) or in_array($field_name, $pk))
			and ! $auto_inc and !isset($field_default)) {
				$required_field_names[] = $field_name_exported;
			}
			
			$columnInfo = array(
				array($type_name, $type_display_range, $type_modifiers, $type_unsigned),
				$field_null,
				$table_col['Key'],
				$table_col['Default']
			);
			$columnInfo_php = var_export($columnInfo, true);
			$columnInfo_js = json_encode($columnInfo);
			$functions["column_$field_name_safe"]['comment'] = <<<EOT
	$dc
	 * Returns schema information for $field_name column
	 * @return {array} [[typeName, displayRange, modifiers, unsigned], isNull, key, default]
	 */
EOT;
			$functions["column_$field_name_safe"]['static'] = true;
			$functions["column_$field_name_safe"]['args'] = '';
			$functions["column_$field_name_safe"]['return_statement'] = <<<EOT
return $columnInfo_php;
EOT;
			$js_functions["column_$field_name_safe"]['static'] = true;
			$js_functions["column_$field_name_safe"]['comment'] = <<<EOT
	$dc
	 * Returns schema information for $field_name column
	 * @return {array} [[typeName, displayRange, modifiers, unsigned], isNull, key, default]
	 */
EOT;
			$js_functions["column_$field_name_safe"]['args'] = '';
			$js_functions["column_$field_name_safe"]['return_statement'] = <<<EOT
return $columnInfo_js;
EOT;
		
		}
		
		$field_names_json = json_encode($field_names);
		$field_names_json_indented = str_replace(
			array("[", ",", "]"),
			array("[\n\t\t", ",\n\t\t", "\n\t]"),
			$field_names_json
		);
		
		
		$field_names_exported = "\$this->fieldNames()";
		
		$functions['beforeSave'] = array();
		$js_functions['beforeSave'] = array();
		if ($required_field_names) {
			$required_fields_string = implode(',', $required_field_names);
			$beforeSave_code = <<<EOT
		if (!\$this->retrieved) {
			\$table = \$this->getTable();
			foreach (array($required_fields_string) as \$name) {
				if (!isset(\$value[\$name])) {
					throw new Exception("the field \$table.\$name needs a value, because it is NOT NULL, not auto_increment, and lacks a default value.");
				}
			}
		}
EOT;
			$js_beforeSave_code = <<<EOT
	var fields = [$required_fields_string], i;
	if (!this._retrieved) {
		var table = this.table();
		for (i=0; i<fields.length; i++) {
			if (this.fields[fields[i]] === undefined) {
				throw new Error("the field "+table+"."+fields[i]+" needs a value, because it is NOT NULL, not auto_increment, and lacks a default value.");
			}
		}
	}
EOT;
			$return_statement = <<<EOT
		return \$value;
EOT;
			$js_return_statement = <<<EOT
	return value;
EOT;
			$functions["beforeSave"][] = $beforeSave_code;
			$functions['beforeSave']['return_statement'] = $return_statement;
			$functions['beforeSave']['comment'] = <<<EOT
	$dc
	 * Check if mandatory fields are set and updates 'magic fields' with appropriate values
	 * @method beforeSave
	 * @param {array} \$value The array of fields
	 * @return {array}
	 * @throws {Exception} If mandatory field is not set
	 */
EOT;
			$js_functions["beforeSave"][] = $js_beforeSave_code;
			$js_functions['beforeSave']['return_statement'] = $js_return_statement;
			$js_functions['beforeSave']['comment'] = <<<EOT
$dc
 * Check if mandatory fields are set and updates 'magic fields' with appropriate values
 * @method beforeSave
 * @param {Object} value The object of fields
 * @param {Function} callback Call this callback if you return null
 * @return {Object|null} Return the fields, modified if necessary. If you return null, then you should call the callback(err, modifiedFields)
 * @throws {Error} If e.g. mandatory field is not set or a bad values are supplied
 */
EOT;
		}

		//$functions['beforeSave'] = array();
		if (count($magic_field_names) > 0) {
			$beforeSave_code = '';
			$js_beforeSave_code = '';
			foreach (array('created_time', 'insertedTime') as $cmf) {
				if (in_array($cmf, $magic_field_names)) {
					$beforeSave_code .= <<<EOT

		if (!\$this->retrieved and !isset(\$value['$cmf'])) {
			\$this->$cmf = \$value['$cmf'] = new Db_Expression('CURRENT_TIMESTAMP');
		}

EOT;
					$js_beforeSave_code .= <<<EOT

	if (!this._retrieved && !value['$cmf']) {
		this['$cmf'] = value['$cmf'] = new Db.Expression('CURRENT_TIMESTAMP');
	}
EOT;
					break;
				}
			}
			foreach (array('updated_time', 'updatedTime') as $umf) {
				if (in_array($umf, $magic_field_names)) {
					$beforeSave_code .= <<<EOT
						
		// convention: we'll have $umf = $cmf if just created.
		\$this->$umf = \$value['$umf'] = new Db_Expression('CURRENT_TIMESTAMP');
EOT;
					$js_beforeSave_code .= <<<EOT

	// convention: we'll have $umf = $cmf if just created.
	this['$umf'] = value['$umf'] = new Db.Expression('CURRENT_TIMESTAMP');
EOT;
					break;
				}
			}
			$return_statement = <<<EOT
		return \$value;
EOT;
			$js_return_statement = <<<EOT
	return value;
EOT;
			$functions['beforeSave'][] = $beforeSave_code;
			$functions['beforeSave']['return_statement'] = $return_statement;
			$js_functions['beforeSave'][] = $js_beforeSave_code;
			$js_functions['beforeSave']['return_statement'] = $js_return_statement;
		}
		
		$functions['fieldNames'] = array();
		$fieldNames_exported = Db_Utils::var_export($field_names);
		$fieldNames_code = <<<EOT
		\$field_names = $fieldNames_exported;
		\$result = \$field_names;
		if (!empty(\$table_alias)) {
			\$temp = array();
			foreach (\$result as \$field_name)
				\$temp[] = \$table_alias . '.' . \$field_name;
			\$result = \$temp;
		} 
		if (!empty(\$field_alias_prefix)) {
			\$temp = array();
			reset(\$field_names);
			foreach (\$result as \$field_name) {
				\$temp[\$field_alias_prefix . current(\$field_names)] = \$field_name;
				next(\$field_names);
			}
			\$result = \$temp;
		}
EOT;
		$return_statement = <<<EOT
		return \$result;
EOT;
		$functions['fieldNames'][] = $fieldNames_code;
		$functions['fieldNames']['return_statement'] = $return_statement;
		$functions['fieldNames']['args'] = '$table_alias = null, $field_alias_prefix = null';
		$functions['fieldNames']['static'] = true;
		$functions['fieldNames']['comment'] = <<<EOT
	$dc
	 * Retrieves field names for class table
	 * @method fieldNames
	 * @static
	 * @param {string} [\$table_alias=null] If set, the alieas is added to each field
	 * @param {string} [\$field_alias_prefix=null] If set, the method returns associative array of ('prefixed field' => 'field') pairs
	 * @return {array} An array of field names
	 */
EOT;
		$functions_code = array();
		foreach ($functions as $func_name => $func_code) {
			$func_args = isset($func_code['args']) ? $func_code['args'] : '$value';
			$func_modifiers = !empty($func_code['static']) ? 'static ' : '';
			$func_code_string = isset($func_code['comment']) ? $func_code['comment']."\n" : '';
			$func_code_string .= <<<EOT
	{$func_modifiers}function $func_name($func_args)
	{

EOT;
			if (is_array($func_code) and ! empty($func_code)) {
				foreach ($func_code as $key => $code_tool) {
					if (is_string($key))
						continue;
					$func_code_string .= $code_tool;
				}
				$func_code_string .= "\n" . $func_code['return_statement'];
			}
			$func_code_string .= <<<EOT
			
	}
EOT;
			if (! empty($func_code))
				$functions_code[] = $func_code_string;
		}
		$functions_string = implode("\n\n", $functions_code);
	
		foreach ($js_functions as $func_name => $func_code) {
			$func_args = isset($func_code['args']) ? $func_code['args'] : 'value';
			$instance = isset($func_code['instance']) ? '.prototype' : '';
			$func_code_string = isset($func_code['comment']) ? $func_code['comment']."\n" : '';
			$prototype = empty($func_code['static']) ? 'prototype.' : '';
			$func_code_string .= <<<EOT
Base.$prototype$func_name = function ($func_args) {

EOT;
			if (is_array($func_code) and ! empty($func_code)) {
				foreach ($func_code as $key => $code_tool) {
					if (is_string($key))
						continue;
					$func_code_string .= $code_tool;
				}
				$func_code_string .= "\n" . $func_code['return_statement'];
			}
			$func_code_string .= <<<EOT

};
EOT;
			if (! empty($func_code))
				$js_functions_code[] = $func_code_string;
		}
		$js_functions_string = implode("\n\n", $js_functions_code);

		$pk_exported_indented = str_replace("\n", "\n\t\t\t", $pk_exported);
		$pk_json_indented = str_replace(
			array("[", ",", "]"),
			array("[\n\t\t", ",\n\t\t", "\n\t]"),
			$pk_json
		);
		$connectionName_var = var_export($connectionName, true);
		$class_name_var = var_export($class_name, true);

		$class_name_prefix = rtrim(ucfirst($classname_prefix), "._");

		$properties2 = array();
		$js_properties2 = array();
		foreach ($properties as $k => $v) {
			$tmp = explode(' ', $v);
			$default = $defaults[$k];
			$comment = str_replace('*/', '**', $comments[$k]);
			$properties[$k] = <<<EOT
	$dc
	 * @property \${$tmp[1]}
	 * @type $tmp[0]
	 * @default $default
	 * $comment
	 */
EOT;
			$required = !$field_nulls[$k] && $default == '"null"';
			$properties2[$k] = $required
				? " * @param {".$tmp[0]."} \$fields.".$tmp[1]
				: " * @param {".$tmp[0]."} [\$fields.".$tmp[1]."] defaults to $default";
		}
		foreach ($js_properties as $k => $v) {
			$tmp = explode(' ', $v);
			$default = $defaults[$k];
			$comment = str_replace('*/', '**', $comments[$k]);
			$js_properties[$k] = <<<EOT
$dc
 * @property $tmp[1]
 * @type $tmp[0]
 * @default $default
 * $comment
 */
EOT;
			$required = !$field_nulls[$k] && $default == '"null"';
			$js_properties2[$k] = $required_fields_string
				? " * @param {".$tmp[0]."} \$fields.".$tmp[1]
				: " * @param {".$tmp[0]."} [\$fields.".$tmp[1]."] defaults to $default";
		}
		$field_hints = implode("\n", $properties);
		$field_hints2 = implode("\n", $properties2);
		$js_field_hints = implode("\n", $js_properties);
		$js_field_hints2 = implode("\n", $properties2);
		
		// Here is the base class:
		$base_class_string = <<<EOT
<?php

$dc
 * Autogenerated base class representing $table_name_base rows
 * in the $connectionName database.
 *
 * Don't change this file, since it can be overwritten.
 * Instead, change the $class_name.php file.
 *
 * @module $connectionName
 */
$dc
 * Base class representing '$class_name_base' rows in the '$connectionName' database
 * @class Base_$class_name
 * @extends Db_Row
 *
 * @param {array} [\$fields=array()] The fields values to initialize table row as 
 * an associative array of \$column => \$value pairs
$field_hints2
 */
abstract class Base_$class_name extends Db_Row
{
$field_hints
	$dc
	 * The setUp() method is called the first time
	 * an object of this class is constructed.
	 * @method setUp
	 */
	function setUp()
	{
		\$this->setDb(self::db());
		\$this->setTable(self::table());
		\$this->setPrimaryKey(
			$pk_exported_indented
		);
	}

	$dc
	 * Connects to database
	 * @method db
	 * @static
	 * @return {Db_Interface} The database object
	 */
	static function db()
	{
		return Db::connect($connectionName_var);
	}

	$dc
	 * Retrieve the table name to use in SQL statement
	 * @method table
	 * @static
	 * @param {boolean} [\$with_db_name=true] Indicates wheather table name should contain the database name
 	 * @return {string|Db_Expression} The table name as string optionally without database name if no table sharding
	 * was started or Db_Expression class with prefix and database name templates is table was sharded
	 */
	static function table(\$with_db_name = true)
	{
		if (Q_Config::get('Db', 'connections', '$connectionName', 'indexes', '$class_name_base', false)) {
			return new Db_Expression((\$with_db_name ? '{\$dbname}.' : '').'{\$prefix}'.'$table_name_base');
		} else {
			\$conn = Db::getConnection($connectionName_var);
  			\$prefix = empty(\$conn['prefix']) ? '' : \$conn['prefix'];
  			\$table_name = \$prefix . '$table_name_base';
  			if (!\$with_db_name)
  				return \$table_name;
  			\$db = Db::connect($connectionName_var);
  			return \$db->dbName().'.'.\$table_name;
		}
	}
	$dc
	 * The connection name for the class
	 * @method connectionName
	 * @static
	 * @return {string} The name of the connection
	 */
	static function connectionName()
	{
		return $connectionName_var;
	}

	$dc
	 * Create SELECT query to the class table
	 * @method select
	 * @static
	 * @param {string|array} [\$fields=null] The fields as strings, or array of alias=>field.
	 *   The default is to return all fields of the table.
	 * @param {string|array} [\$alias=null] The tables as strings, or array of alias=>table.
	 * @return {Db_Query_Mysql} The generated query
	 */
	static function select(\$fields=null, \$alias = null)
	{
		if (!isset(\$fields)) {
			\$fieldNames = array();
			foreach (self::fieldNames() as \$fn) {
				\$fieldNames[] = \$fn;
			}
			\$fields = implode(',', \$fieldNames);
		}
		if (!isset(\$alias)) \$alias = '';
		\$q = self::db()->select(\$fields, self::table().' '.\$alias);
		\$q->className = $class_name_var;
		return \$q;
	}

	$dc
	 * Create UPDATE query to the class table
	 * @method update
	 * @static
	 * @param {string} [\$alias=null] Table alias
	 * @return {Db_Query_Mysql} The generated query
	 */
	static function update(\$alias = null)
	{
		if (!isset(\$alias)) \$alias = '';
		\$q = self::db()->update(self::table().' '.\$alias);
		\$q->className = $class_name_var;
		return \$q;
	}

	$dc
	 * Create DELETE query to the class table
	 * @method delete
	 * @static
	 * @param {object} [\$table_using=null] If set, adds a USING clause with this table
	 * @param {string} [\$alias=null] Table alias
	 * @return {Db_Query_Mysql} The generated query
	 */
	static function delete(\$table_using = null, \$alias = null)
	{
		if (!isset(\$alias)) \$alias = '';
		\$q = self::db()->delete(self::table().' '.\$alias, \$table_using);
		\$q->className = $class_name_var;
		return \$q;
	}

	$dc
	 * Create INSERT query to the class table
	 * @method insert
	 * @static
	 * @param {object} [\$fields=array()] The fields as an associative array of column => value pairs
	 * @param {string} [\$alias=null] Table alias
	 * @return {Db_Query_Mysql} The generated query
	 */
	static function insert(\$fields = array(), \$alias = null)
	{
		if (!isset(\$alias)) \$alias = '';
		\$q = self::db()->insert(self::table().' '.\$alias, \$fields);
		\$q->className = $class_name_var;
		return \$q;
	}
	
	$dc
	 * Inserts multiple rows into a single table, preparing the statement only once,
	 * and executes all the queries.
	 * @method insertManyAndExecute
	 * @static
	 * @param {array} [\$rows=array()] The array of rows to insert. 
	 * (The field names for the prepared statement are taken from the first row.)
	 * You cannot use Db_Expression objects here, because the function binds all parameters with PDO.
	 * @param {array} [\$options=array()]
	 *   An associative array of options, including:
	 *
	 * * "chunkSize" {integer} The number of rows to insert at a time. defaults to 20.<br>
	 * * "onDuplicateKeyUpdate" {array} You can put an array of fieldname => value pairs here,
	 * 		which will add an ON DUPLICATE KEY UPDATE clause to the query.
	 *
	 */
	static function insertManyAndExecute(\$rows = array(), \$options = array())
	{
		self::db()->insertManyAndExecute(
			self::table(), \$rows,
			array_merge(\$options, array('className' => $class_name_var))
		);
	}
	
	$dc
	 * Create raw query with begin clause
	 * You'll have to specify shards yourself when calling execute().
	 * @method begin
	 * @static
	 * @param {string} [\$lockType=null] First parameter to pass to query->begin() function
	 * @return {Db_Query_Mysql} The generated query
	 */
	static function begin(\$lockType = null)
	{
		\$q = self::db()->rawQuery('')->begin(\$lockType);
		\$q->className = $class_name_var;
		return \$q;
	}
	
	$dc
	 * Create raw query with commit clause
	 * You'll have to specify shards yourself when calling execute().
	 * @method commit
	 * @static
	 * @return {Db_Query_Mysql} The generated query
	 */
	static function commit()
	{
		\$q = self::db()->rawQuery('')->commit();
		\$q->className = $class_name_var;
		return \$q;
	}
	
	$dc
	 * Create raw query with rollback clause
	 * @method rollback
	 * @static
	 * @param {array} \$criteria Can be used to target the rollback to some shards.
	 *  Otherwise you'll have to specify shards yourself when calling execute().
	 * @return {Db_Query_Mysql} The generated query
	 */
	static function rollback()
	{
		\$q = self::db()->rawQuery('')->rollback();
		\$q->className = $class_name_var;
		return \$q;
	}
	
$functions_string
};
EOT;

		// Set the JS code
		$js_code = <<<EOT
$dc
 * Autogenerated base class representing $table_name_base rows
 * in the $connectionName database.
 *
 * Don't change this file, since it can be overwritten.
 * Instead, change the $class_name_prefix/$class_name_base.js file.
 *
 * @module $connectionName
 */

var Q = require('Q');
var Db = Q.require('Db');
var $connectionName = Q.require('$connectionName');
var Row = Q.require('Db/Row');

$dc
 * Base class representing '$class_name_base' rows in the '$connectionName' database
 * @namespace Base.$class_name_prefix
 * @class $class_name_base
 * @extends Db.Row
 * @constructor
 * @param {object} [fields={}] The fields values to initialize table row as 
 * an associative array of {column: value} pairs
$field_hints2
 */
function Base (fields) {
	Base.constructors.apply(this, arguments);
}

Q.mixin(Base, Row);

$js_field_hints

$dc
 * This method calls Db.connect() using information stored in the configuration.
 * If this has already been called, then the same db object is returned.
 * @method db
 * @return {Db} The database connection
 */
Base.db = function () {
	return $connectionName.db();
};

$dc
 * Retrieve the table name to use in SQL statements
 * @method table
 * @param {boolean} [withoutDbName=false] Indicates wheather table name should contain the database name
 * @return {String|Db.Expression} The table name as string optionally without database name if no table sharding was started
 * or Db.Expression object with prefix and database name templates is table was sharded
 */
Base.table = function (withoutDbName) {
	if (Q.Config.get(['Db', 'connections', '$connectionName', 'indexes', '$class_name_base'], false)) {
		return new Db.Expression((withoutDbName ? '' : '{\$dbname}.')+'{\$prefix}$table_name_base');
	} else {
		var conn = Db.getConnection('$connectionName');
		var prefix = conn.prefix || '';
		var tableName = prefix + '$table_name_base';
		var dbname = Base.table.dbname;
		if (!dbname) {
			var dsn = Db.parseDsnString(conn['dsn']);
			dbname = Base.table.dbname = dsn.dbname;
		}
		return withoutDbName ? tableName : dbname + '.' + tableName;
	}
};

$dc
 * The connection name for the class
 * @method connectionName
 * @return {String} The name of the connection
 */
Base.connectionName = function() {
	return '$connectionName';
};

$dc
 * Create SELECT query to the class table
 * @method SELECT
 * @param {String|Object} [fields=null] The fields as strings, or object of {alias:field} pairs.
 *   The default is to return all fields of the table.
 * @param {String|Object} [alias=null] The tables as strings, or object of {alias:table} pairs.
 * @return {Db.Query.Mysql} The generated query
 */
Base.SELECT = function(fields, alias) {
	if (!fields) {
		fields = Base.fieldNames().map(function (fn) {
			return fn;
		}).join(',');
	}
	var q = Base.db().SELECT(fields, Base.table()+(alias ? ' '+alias : ''));
	q.className = '$class_name';
	return q;
};

$dc
 * Create UPDATE query to the class table. Use Db.Query.Mysql.set() method to define SET clause
 * @method UPDATE
 * @param {String} [alias=null] Table alias
 * @return {Db.Query.Mysql} The generated query
 */
Base.UPDATE = function(alias) {
	var q = Base.db().UPDATE(Base.table()+(alias ? ' '+alias : ''));
	q.className = '$class_name';
	return q;
};

$dc
 * Create DELETE query to the class table
 * @method DELETE
 * @param {Object}[table_using=null] If set, adds a USING clause with this table
 * @param {String} [alias=null] Table alias
 * @return {Db.Query.Mysql} The generated query
 */
Base.DELETE = function(table_using, alias) {
	var q = Base.db().DELETE(Base.table()+(alias ? ' '+alias : ''), table_using);
	q.className = '$class_name';
	return q;
};

$dc
 * Create INSERT query to the class table
 * @method INSERT
 * @param {Object} [fields={}] The fields as an associative array of {column: value} pairs
 * @param {String} [alias=null] Table alias
 * @return {Db.Query.Mysql} The generated query
 */
Base.INSERT = function(fields, alias) {
	var q = Base.db().INSERT(Base.table()+(alias ? ' '+alias : ''), fields || {});
	q.className = '$class_name';
	return q;
};

$dc
 * Create raw query with BEGIN clause.
 * You'll have to specify shards yourself when calling execute().
 * @method BEGIN
 * @param {string} [\$lockType] First parameter to pass to query.begin() function
 * @return {Db.Query.Mysql} The generated query
 */
Base.BEGIN = function(\$lockType) {
	var q = Base.db().rawQuery('').begin(\$lockType);
	q.className = '$class_name';
	return q;
};

$dc
 * Create raw query with COMMIT clause
 * You'll have to specify shards yourself when calling execute().
 * @method COMMIT
 * @return {Db.Query.Mysql} The generated query
 */
Base.COMMIT = function() {
	var q = Base.db().rawQuery('').commit();
	q.className = '$class_name';
	return q;
};

$dc
 * Create raw query with ROLLBACK clause
 * @method ROLLBACK
 * @param {Object} criteria can be used to target the query to some shards.
 *   Otherwise you'll have to specify shards yourself when calling execute().
 * @return {Db.Query.Mysql} The generated query
 */
Base.ROLLBACK = function(criteria) {
	var q = Base.db().rawQuery('').rollback(crieria);
	q.className = '$class_name';
	return q;
};

$dc
 * The name of the class
 * @property className
 * @type string
 */
Base.prototype.className = "$class_name";

// Instance methods

$dc
 * Create INSERT query to the class table
 * @method INSERT
 * @param {object} [fields={}] The fields as an associative array of {column: value} pairs
 * @param {string} [alias=null] Table alias
 * @return {Db.Query.Mysql} The generated query
 */
Base.prototype.setUp = function() {
	// does nothing for now
};

$dc
 * Create INSERT query to the class table
 * @method INSERT
 * @param {object} [fields={}] The fields as an associative array of {column: value} pairs
 * @param {string} [alias=null] Table alias
 * @return {Db.Query.Mysql} The generated query
 */
Base.prototype.db = function () {
	return Base.db();
};

$dc
 * Retrieve the table name to use in SQL statements
 * @method table
 * @param {boolean} [withoutDbName=false] Indicates wheather table name should contain the database name
 * @return {String|Db.Expression} The table name as string optionally without database name if no table sharding was started
 * or Db.Expression object with prefix and database name templates is table was sharded
 */
Base.prototype.table = function () {
	return Base.table();
};

$dc
 * Retrieves primary key fields names for class table
 * @method primaryKey
 * @return {string[]} An array of field names
 */
Base.prototype.primaryKey = function () {
	return $pk_json_indented;
};

$dc
 * Retrieves field names for class table
 * @method fieldNames
 * @return {array} An array of field names
 */
Base.prototype.fieldNames = function () {
	return Base.fieldNames();
};

$dc
 * Retrieves field names for class table
 * @method fieldNames
 * @static
 * @return {array} An array of field names
 */
Base.fieldNames = function () {
	return $field_names_json_indented;
};

$js_functions_string

module.exports = Base;
EOT;

		// Return the base class	
		return $base_class_string; // unless the above line threw an exception
	}
}