<commit_before><?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, $connectionName, $shardName);
		$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.
		
		$columnsList = array();
		$valuesList = array();
		foreach ($fields as $column => $value) {
			$columnsList[] = Db_Query_Mysql::column($column);
			if ($value instanceof Db_Expression) {
				$valuesList[] = "$value";
			} else {
				$valuesList[] = ":$column";
			}
		}
		$columnsString = implode(', ', $columnsList);
		$valuesString = implode(', ', $valuesList);
		
		$clauses = array(
			'INTO' => "$table_into ($columnsString)", 'VALUES' => $valuesString
		);
		
		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 {array} [$options.columns] Pass an array of column names, otherwise
	 *    they are automatically taken from the first row being inserted.
	 * @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
		$rawColumns = array();
		if (isset($options['columns'])) {
			$columnsList = $options['columns'];
			foreach ($columnsList as $c) {
				$rawColumns[$c] = $c;
			}
		} else {
			$row = reset($rows);
			$record = ($row instanceof Db_Row) ? $row->fields : $row;
			foreach ($record as $column => $value) {
				$columnsList[] = $c = Db_Query_Mysql::column($column);
				$rawColumns[$c] = $column;
			}
		}
		$columnsString = implode(', ', $columnsList);
		$into = "$table_into ($columnsString)";
		
		// 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);<commit_msg>Modified method Db_Mysql::insertManyAndExecute. Call event Db/Row/$className/saveExecute before row inserted to db.<commit_after><?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, $connectionName, $shardName);
		$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.
		
		$columnsList = array();
		$valuesList = array();
		foreach ($fields as $column => $value) {
			$columnsList[] = Db_Query_Mysql::column($column);
			if ($value instanceof Db_Expression) {
				$valuesList[] = "$value";
			} else {
				$valuesList[] = ":$column";
			}
		}
		$columnsString = implode(', ', $columnsList);
		$valuesString = implode(', ', $valuesList);
		
		$clauses = array(
			'INTO' => "$table_into ($columnsString)", 'VALUES' => $valuesString
		);
		
		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 {array} [$options.columns] Pass an array of column names, otherwise
	 *    they are automatically taken from the first row being inserted.
	 * @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
		$rawColumns = array();
		if (isset($options['columns'])) {
			$columnsList = $options['columns'];
			foreach ($columnsList as $c) {
				$rawColumns[$c] = $c;
			}
		} else {
			$row = reset($rows);
			$record = ($row instanceof Db_Row) ? $row->fields : $row;
			foreach ($record as $column => $value) {
				$columnsList[] = $c = Db_Query_Mysql::column($column);
				$rawColumns[$c] = $column;
			}
		}
		$columnsString = implode(', ', $columnsList);
		$into = "$table_into ($columnsString)";
		
		// 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) {
			if (class_exists($className)) {
				/**
				 * Gives an oppotunity to modify the row or do something else
				 * @event {before} Db/Row/$className/save
				 * @param {Db_Row} row
				 */
				Q::event("Db/Row/$className/saveExecute", array(
					'row' => $row
				), 'before');
			}
			// 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);
{'fraction_non_alphanumeric': 0.1507865256255123, 'fraction_numerical': 0.0024200788477302003, 'mean_word_length': 3.7078280044101435, 'pattern_counts': {'":': 2, '<': 7, '<?xml version=': 0, '>': 110, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '791138', 'n_tokens_mistral': 9292, 'n_tokens_neox': 8067, 'n_words': 2417}