<?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
}
}