mirror of
https://github.com/beestat/app.git
synced 2025-05-24 02:14:03 -04:00
Loading now doesn't break if the sync fails, and as a bonus that made it trivial to display whether or not ecobee is down.
908 lines
26 KiB
PHP
908 lines
26 KiB
PHP
<?php
|
|
|
|
namespace cora;
|
|
|
|
/**
|
|
* This exception is thrown by database->query if the query failed due to a
|
|
* duplicate entry in the database.
|
|
*
|
|
* @author Jon Ziebell
|
|
*/
|
|
final class DuplicateEntryException extends \Exception {
|
|
};
|
|
|
|
/**
|
|
* This is a MySQLi database wrapper. It provides access to some basic
|
|
* functions like select, insert, and update. Those functions automatically
|
|
* escape table names, column names, and parameters for you using a number of
|
|
* the private functions defined here.
|
|
*
|
|
* Alternatively, you can write your own queries (and use the escape()
|
|
* function to help), and just call query() to run your own.
|
|
*
|
|
* @author Jon Ziebell
|
|
*/
|
|
final class database extends \mysqli {
|
|
|
|
/**
|
|
* The singleton.
|
|
*
|
|
* @var database
|
|
*/
|
|
private static $instance;
|
|
|
|
/**
|
|
* A database singleton that does not use transactions.
|
|
*
|
|
* @var database
|
|
*/
|
|
private static $transactionless_instance;
|
|
|
|
/**
|
|
* Column types
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $types;
|
|
|
|
/**
|
|
* Whether or not to use transactions in this connection.
|
|
*
|
|
* @var bool
|
|
*/
|
|
private $use_transactions = true;
|
|
|
|
/**
|
|
* Whether or not a transaction has been started. Used to make sure only one
|
|
* is started at a time and it gets closed only if it's open.
|
|
*
|
|
* @var bool
|
|
*/
|
|
private $transaction_started = false;
|
|
|
|
/**
|
|
* The total number of queries executed.
|
|
*
|
|
* @var int
|
|
*/
|
|
private $query_count = 0;
|
|
|
|
/**
|
|
* The total time all queries have taken to execute.
|
|
*
|
|
* @var float
|
|
*/
|
|
private $query_time = 0;
|
|
|
|
/**
|
|
* The cora object.
|
|
*
|
|
* @var cora
|
|
*/
|
|
private $cora;
|
|
|
|
/**
|
|
* The setting object.
|
|
*
|
|
* @var setting
|
|
*/
|
|
private $setting;
|
|
|
|
/**
|
|
* Create the mysql object used for the current API call and start a
|
|
* transaction. The same transaction is used for all queries on this
|
|
* connection, even in the case of a multi-api call. The transaction is
|
|
* auto- closed upon destruction of this class.
|
|
*
|
|
* This function is private because this class is a singleton and should be
|
|
* instantiated using the get_instance() function.
|
|
*
|
|
* @throws \Exception If failing to connect to the database.
|
|
*/
|
|
public function __construct() {
|
|
$this->cora = cora::get_instance();
|
|
$this->setting = setting::get_instance();
|
|
|
|
parent::__construct(
|
|
$this->setting->get('database_host'),
|
|
$this->setting->get('database_username'),
|
|
$this->setting->get('database_password')
|
|
);
|
|
|
|
// Have the database driver return ints and floats in PHP native types. PHP
|
|
// does not have a native type for decimals so that gets left behind.
|
|
parent::options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, true);
|
|
|
|
if($this->connect_error !== null) {
|
|
$this->cora->set_error_extra_info(
|
|
[
|
|
'database_error' => $this->connect_error
|
|
]
|
|
);
|
|
throw new \Exception('Could not connect to database.', 1200);
|
|
}
|
|
|
|
$database_name = $this->setting->get('database_name');
|
|
if($database_name !== null) {
|
|
$success = $this->select_db($database_name);
|
|
if($success === false) {
|
|
|
|
$this->cora->set_error_extra_info(
|
|
[
|
|
'database_error' => $this->error
|
|
]
|
|
);
|
|
throw new \Exception('Could not select database.', 1208);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Upon destruction of this class, close the open transaction. I check to
|
|
* make sure one is open, but that should really always be the case since
|
|
* one gets opened regardless.
|
|
*/
|
|
public function __destruct() {
|
|
if($this->transaction_started === true) {
|
|
$this->commit_transaction();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use this function to instantiate this class instead of calling new
|
|
* database() (which isn't allowed anyways). This avoids confusion from
|
|
* trying to use dependency injection by passing an instance of this class
|
|
* around everywhere. It also keeps a single connection open to the database
|
|
* for the current API call.
|
|
*
|
|
* @return database A new database object or the already created one.
|
|
*/
|
|
public static function get_instance() {
|
|
if(isset(self::$instance) === false) {
|
|
self::$instance = new self();
|
|
}
|
|
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Second instance; see comment on the class member declaration.
|
|
*
|
|
* @return database A new database object or the already created one.
|
|
*/
|
|
public static function get_transactionless_instance() {
|
|
if(isset(self::$transactionless_instance) === false) {
|
|
self::$transactionless_instance = new self();
|
|
self::$transactionless_instance->disable_transactions();
|
|
}
|
|
|
|
return self::$transactionless_instance;
|
|
}
|
|
|
|
/**
|
|
* Start a transaction.
|
|
*
|
|
* @throws \Exception If the transaction fails to start.
|
|
*/
|
|
public function start_transaction() {
|
|
if($this->transaction_started === false) {
|
|
$result = $this->query('start transaction');
|
|
if($result === false) {
|
|
throw new \Exception('Failed to start database transaction.', 1201);
|
|
}
|
|
$this->transaction_started = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Commit a transaction.
|
|
*
|
|
* @throws \Exception If the transaction fails to commit.
|
|
*/
|
|
public function commit_transaction() {
|
|
if($this->transaction_started === true) {
|
|
$this->transaction_started = false;
|
|
$result = $this->query('commit');
|
|
if($result === false) {
|
|
throw new \Exception('Failed to commit database transaction.', 1202);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rollback the current transaction. This is exposed because the exception
|
|
* handler needs to rollback the current transaction when it runs.
|
|
*
|
|
* @throws \Exception If the transaction fails to rollback.
|
|
*/
|
|
public function rollback_transaction() {
|
|
if($this->transaction_started === true) {
|
|
$this->transaction_started = false;
|
|
$result = $this->query('rollback');
|
|
if($result === false) {
|
|
throw new \Exception('Failed to rollback database transaction.', 1203);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Escape a value to be used in a query. Only necessary when doing custom
|
|
* queries. All helper functions like select, insert, and update escape
|
|
* values for you using this function.
|
|
*
|
|
* @param mixed $value The value to escape. Boolean true and false are
|
|
* converted to int 1 and 0 respectively.
|
|
* @param bool $basic If overridden to true, just return real_escape_string
|
|
* of $value. If left alone or set to false, return a value appropriate to
|
|
* be used like "set foo=$bar" as it will have single quotes around it if
|
|
* necessary.
|
|
*
|
|
* @return string The escaped value.
|
|
*/
|
|
public function escape($value, $basic = false) {
|
|
if($basic === true) {
|
|
return $this->real_escape_string($value);
|
|
}
|
|
|
|
if($value === null) {
|
|
return 'null';
|
|
}
|
|
else if($value === true) {
|
|
return '1';
|
|
}
|
|
else if($value === false) {
|
|
return '0';
|
|
}
|
|
else if(is_int($value) === true) {
|
|
return $value;
|
|
}
|
|
else {
|
|
return '"' . $this->real_escape_string($value) . '"';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to secure names of tables & columns passed to this class.
|
|
* First of all, these identifiers must be a valid word. Backticks are also
|
|
* placed around the identifier in all cases to allow the use of MySQL
|
|
* keywords as table and column names.
|
|
*
|
|
* @param string $identifier The identifier to escape
|
|
*
|
|
* @throws \Exception If the identifier does not match the character class
|
|
* [A-Za-z0-9_]. That would make it invalid for use in MySQL.
|
|
*
|
|
* @return string The escaped identifier.
|
|
*/
|
|
public function escape_identifier($identifier) {
|
|
if(preg_match('/^\w+$/', $identifier) === 1) {
|
|
return '`' . $identifier . '`';
|
|
}
|
|
else {
|
|
$this->cora->set_error_extra_info(
|
|
[
|
|
'identifier' => $identifier
|
|
]
|
|
);
|
|
throw new \Exception('Query identifier is invalid.', 1204);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds a properly escaped string for the 'where column=value' portion of
|
|
* a query.
|
|
*
|
|
* @param string $column The query column.
|
|
* @param mixed $value The value(s) to compare against. You can use null, an
|
|
* array, or any other value here and the appropriate comparison (is null,
|
|
* in, =) will be used.
|
|
*
|
|
* @throws \Exception If an invalid operator was specified.
|
|
*
|
|
* @return string The appropriate escaped string. Examples: `foo` is null
|
|
* `foo` in(1,2,3) `foo`='bar'
|
|
*/
|
|
private function column_equals_value_where($column, $value) {
|
|
if($value === null) {
|
|
return $this->escape_identifier($column) . ' is null';
|
|
}
|
|
else if(is_array($value) === true) {
|
|
if(isset($value['operator']) === true) {
|
|
if(in_array($value['operator'], ['>', '<', '=', '>=', '<=', 'between']) === false) {
|
|
throw new \Exception('Invalid operator', 1213);
|
|
}
|
|
if($value['operator'] === 'between') {
|
|
return $this->escape_identifier($column) . ' between ' . $this->escape($value['value'][0]) . ' and ' . $this->escape($value['value'][1]);
|
|
} else {
|
|
return $this->escape_identifier($column) . $value['operator'] . $this->escape($value['value']);
|
|
}
|
|
}
|
|
else {
|
|
return $this->escape_identifier($column) .
|
|
' in (' . implode(',', array_map([$this, 'escape'], $value)) . ')';
|
|
}
|
|
}
|
|
else {
|
|
return $this->escape_identifier($column) . '=' . $this->escape($value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds a properly escaped string for the 'set column=value' portion of a
|
|
* query.
|
|
*
|
|
* @param string $column The query column.
|
|
* @param mixed $value The value to set.
|
|
*
|
|
* @return string The appropriate escaped string. Examples: `foo`='bar'
|
|
* `foo`=5
|
|
*/
|
|
private function column_equals_value_set($column, $value) {
|
|
return $this->escape_identifier($column) . '=' . $this->escape($value);
|
|
}
|
|
|
|
/**
|
|
* Performs a query on the database. This function is available publicly for
|
|
* the case when the standard select, insert, and update don't quite cut it.
|
|
*
|
|
* The exceptions are broken up somewhat by type to make it easier to catch
|
|
* and handle these exceptions if desired.
|
|
*
|
|
* This will start a transaction if the query begins with 'insert',
|
|
* 'update', or 'delete' and a transaction has not already been started.
|
|
*
|
|
* IMPORTANT: YOU MUST SANTIZE YOUR OWN DATABASE QUERY WHEN USING THIS
|
|
* FUNCTION DIRECTLY. THIS FUNCTION DOES NOT DO IT FOR YOU.
|
|
*
|
|
* @param string $query The query to execute.
|
|
* @param int $resultmode Just here because PHP requires it.
|
|
*
|
|
* @throws DuplicateEntryException if the query failed due to a duplicate
|
|
* entry (unique key violation)
|
|
* @throws \Exception If the query failed and was not caught by any other
|
|
* exception types.
|
|
*
|
|
* @return mixed The result directly from $mysqli->query.
|
|
*/
|
|
public function query($query, $resultmode = null) {
|
|
// If this was an insert, update or delete, start a transaction
|
|
$query_type = substr(trim($query), 0, 6);
|
|
if(
|
|
in_array($query_type, ['insert', 'update', 'delete']) === true &&
|
|
$this->use_transactions === true
|
|
) {
|
|
$this->start_transaction();
|
|
}
|
|
|
|
$start = microtime(true);
|
|
$result = parent::query($query);
|
|
$stop = microtime(true);
|
|
|
|
if($result === false) {
|
|
$database_error = $this->error;
|
|
$this->rollback_transaction();
|
|
|
|
$this->cora->set_error_extra_info(
|
|
[
|
|
'database_error' => $database_error,
|
|
'query' => $query
|
|
]
|
|
);
|
|
|
|
if(stripos($database_error, 'duplicate entry') !== false) {
|
|
throw new DuplicateEntryException('Duplicate database entry.', 1205);
|
|
}
|
|
else {
|
|
throw new \Exception('Database query failed.', 1206);
|
|
}
|
|
}
|
|
|
|
// Don't log info about transactions...they're a wash
|
|
$this->query_count++;
|
|
$this->query_time += ($stop - $start);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Select some columns from some table with some where clause.
|
|
*
|
|
* @param string $resource The resource to select from.
|
|
* @param array $attributes An array of key value pairs to search by and can
|
|
* include arrays if you want to search in() something.
|
|
* @param array $columns The columns to return. If not specified, all
|
|
* columns are returned.
|
|
* @param mixed $order_by String or array of order_bys.
|
|
* @param mixed $limit Number or array of numbers as arguments to the MySQL
|
|
* limit clause.
|
|
*
|
|
* @return array An array of the database rows with the specified columns.
|
|
* Even a single result will still be returned in an array of size 1.
|
|
*/
|
|
public function read($resource, $attributes = [], $columns = [], $order_by = [], $limit = []) {
|
|
$table = $this->get_table($resource);
|
|
|
|
// Build the column listing.
|
|
if(count($columns) === 0) {
|
|
$columns = '*';
|
|
}
|
|
else {
|
|
$columns = implode(
|
|
',',
|
|
array_map([$this, 'escape_identifier'], $columns)
|
|
);
|
|
}
|
|
|
|
// Remove empty arrays from the attributes to avoid query errors on the
|
|
// empty in() statement.
|
|
foreach($attributes as $key => $value) {
|
|
if(is_array($value) === true && count($value) === 0) {
|
|
unset($attributes[$key]);
|
|
}
|
|
}
|
|
|
|
// Build the where clause.
|
|
if (count($attributes) === 0) {
|
|
$where = '';
|
|
} else {
|
|
$where = ' where ' .
|
|
implode(
|
|
' and ',
|
|
array_map(
|
|
[$this, 'column_equals_value_where'],
|
|
array_keys($attributes),
|
|
$attributes
|
|
)
|
|
);
|
|
}
|
|
|
|
// Order by
|
|
if (is_array($order_by) === false) {
|
|
$order_by = [$order_by];
|
|
}
|
|
|
|
if (count($order_by) === 0) {
|
|
$order_by = '';
|
|
} else {
|
|
$order_by = ' order by ' .
|
|
implode(
|
|
',',
|
|
array_map(
|
|
[$this, 'escape_identifier'],
|
|
$order_by
|
|
)
|
|
);
|
|
}
|
|
|
|
// Limit
|
|
if (is_array($limit) === false) {
|
|
$limit = [$limit];
|
|
}
|
|
|
|
if (count($limit) === 0) {
|
|
$limit = '';
|
|
} else {
|
|
$limit = ' limit ' .
|
|
implode(
|
|
',',
|
|
array_map(
|
|
[$this, 'escape'],
|
|
$limit
|
|
)
|
|
);
|
|
}
|
|
|
|
// Put everything together and return the result.
|
|
$query = 'select ' . $columns . ' from ' .
|
|
$this->escape_identifier($table) . $where . $order_by . $limit;
|
|
$result = $this->query($query);
|
|
|
|
/**
|
|
* Get a list of all fields that need to be cast to other data types. The
|
|
* MYSQLI_OPT_INT_AND_FLOAT_NATIVE flag handles ints and floats. This
|
|
* turns decimal into float and tinyint(1) into boolean.
|
|
*/
|
|
$float_fields = [];
|
|
$boolean_fields = [];
|
|
$json_fields = [];
|
|
while($field_info = $result->fetch_field()) {
|
|
if($field_info->type === 1 && $field_info->length === 1) {
|
|
$boolean_fields[] = $field_info->name;
|
|
}
|
|
else if($field_info->type === 246) {
|
|
$float_fields[] = $field_info->name;
|
|
}
|
|
else if($field_info->type === 245) {
|
|
$json_fields[] = $field_info->name;
|
|
}
|
|
}
|
|
|
|
$results = [];
|
|
while($row = $result->fetch_assoc()) {
|
|
// Cast if necessary.
|
|
foreach($float_fields as $float_field) {
|
|
$row[$float_field] = (float) $row[$float_field];
|
|
}
|
|
foreach($boolean_fields as $boolean_field) {
|
|
$row[$boolean_field] = (boolean) $row[$boolean_field];
|
|
}
|
|
foreach($json_fields as $json_field) {
|
|
$row[$json_field] = json_decode($row[$json_field], true);
|
|
}
|
|
|
|
|
|
// Diverge the converged column.
|
|
if(
|
|
isset($row['converged']) === true &&
|
|
class_exists($resource) === true && // This will also call the autoloader to make sure it's loaded
|
|
isset($resource::$converged) === true
|
|
) {
|
|
foreach($resource::$converged as $column => $column_properties) {
|
|
if(isset($row[$column]) === true) {
|
|
throw new \Exception('Column `' . $column . '` exists; cannot be overwritten by converged column.', 1212);
|
|
}
|
|
$row[$column] = (isset($row['converged'][$column]) === true) ? $row['converged'][$column] : null;
|
|
}
|
|
unset($row['converged']);
|
|
}
|
|
|
|
$results[] = $row;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Converge attributes.
|
|
*
|
|
* @param string $resource
|
|
* @param int $id
|
|
* @param array $attributes
|
|
*
|
|
* @return array
|
|
*/
|
|
private function converge_attributes($resource, $id, $attributes) {
|
|
if(
|
|
class_exists($resource) === true && // This will also call the autoloader to make sure it's loaded
|
|
isset($resource::$converged) === true &&
|
|
empty($resource::$converged) === false
|
|
) {
|
|
$table = $this->get_table($resource);
|
|
|
|
if($id !== null) {
|
|
$existing_attributes = [];
|
|
$existing_attributes[$table . '_id'] = $id;
|
|
$existing_row = $this->read($resource, $existing_attributes)[0];
|
|
}
|
|
else {
|
|
$existing_row = [];
|
|
}
|
|
|
|
$converged = [];
|
|
foreach($resource::$converged as $column => $column_properties) {
|
|
if(isset($existing_row[$column]) === true) {
|
|
$converged[$column] = $existing_row[$column];
|
|
}
|
|
if(array_key_exists($column, $attributes) === true) {
|
|
if($attributes[$column] === null) {
|
|
unset($converged[$column]);
|
|
}
|
|
else {
|
|
switch($column_properties['type']) {
|
|
case 'int':
|
|
case 'float':
|
|
case 'string':
|
|
settype($attributes[$column], $column_properties['type']);
|
|
break;
|
|
}
|
|
$converged[$column] = $attributes[$column];
|
|
}
|
|
unset($attributes[$column]);
|
|
}
|
|
}
|
|
$attributes['converged'] = json_encode($converged);
|
|
}
|
|
|
|
return $attributes;
|
|
}
|
|
|
|
/**
|
|
* Update some columns in a table by the primary key. Doing updates without
|
|
* using the primary key are supported by writing your own queries and using
|
|
* the database->query() function. That should be a rare circumstance
|
|
* though.
|
|
*
|
|
* @param string $resource The resource to update.
|
|
* @param array $attributes The attributes to set.
|
|
* @param array $return_mode Either "row" or "id". Specifying row will
|
|
* return the newly created row (does a database read). Specifying id will
|
|
* return just the ID of the created row instead of performing another query
|
|
* to get the whole inserted row.
|
|
*
|
|
* @throws \Exception If no attributes were specified.
|
|
*
|
|
* @return int The updated row.
|
|
*/
|
|
public function update($resource, $attributes, $return_mode = 'row') {
|
|
$table = $this->get_table($resource);
|
|
|
|
foreach($attributes as $key => $value) {
|
|
if($this->get_type($resource, $key) === 'json') {
|
|
if($value === null) {
|
|
$attributes[$key] = null;
|
|
}
|
|
else {
|
|
$attributes[$key] = json_encode($value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for errors
|
|
if(isset($attributes[$table . '_id']) === false) {
|
|
throw new \Exception('ID is required for update.', 1214);
|
|
}
|
|
|
|
// Extract the ID.
|
|
$id = $attributes[$table . '_id'];
|
|
unset($attributes[$table . '_id']);
|
|
|
|
// Check for errors
|
|
if(count($attributes) === 0) {
|
|
throw new \Exception('Updates require at least one attribute.', 1207);
|
|
}
|
|
|
|
// Converge the diverged attributes.
|
|
$attributes = $this->converge_attributes($resource, $id, $attributes);
|
|
|
|
// Build the column setting
|
|
$columns = implode(
|
|
',',
|
|
array_map(
|
|
[$this, 'column_equals_value_set'],
|
|
array_keys($attributes),
|
|
$attributes
|
|
)
|
|
);
|
|
|
|
// Build the where clause
|
|
$where_attributes = [$table . '_id' => $id];
|
|
$where = 'where ' .
|
|
implode(
|
|
' and ',
|
|
array_map(
|
|
[$this, 'column_equals_value_where'],
|
|
array_keys($where_attributes),
|
|
$where_attributes
|
|
)
|
|
);
|
|
|
|
$query = 'update ' . $this->escape_identifier($table) .
|
|
' set ' . $columns . ' ' . $where;
|
|
|
|
// Disallow updates in the demo.
|
|
if($this->setting->is_demo() === true) {
|
|
return $this->read($resource, $where_attributes)[0];
|
|
}
|
|
|
|
$this->query($query);
|
|
|
|
if($return_mode === 'row') {
|
|
return $this->read($resource, $where_attributes)[0];
|
|
} else {
|
|
return $id;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Set deleted = 1 on the database row.
|
|
*
|
|
* @param string $resource The table to delete from.
|
|
* @param int $id The value of the primary key to delete.
|
|
*
|
|
* @return int The number of rows affected by the delete (could be 0).
|
|
*/
|
|
public function delete($resource, $id) {
|
|
$table = $this->get_table($resource);
|
|
|
|
$attributes = [];
|
|
$attributes[$table . '_id'] = $id;
|
|
$attributes['deleted'] = true;
|
|
|
|
$this->update($resource, $attributes);
|
|
|
|
return $this->affected_rows;
|
|
}
|
|
|
|
/**
|
|
* Insert a row into the specified table. This only supports single-row
|
|
* inserts.
|
|
*
|
|
* @param string $table The table to insert into.
|
|
* @param array $attributes The attributes to set on the row.
|
|
* @param array $return_mode Either "row" or "id". Specifying row will
|
|
* return the newly created row (does a database read). Specifying id will
|
|
* return just the ID of the created row.
|
|
*
|
|
* @return int The primary key of the inserted row.
|
|
*/
|
|
public function create($resource, $attributes, $return_mode = 'row') {
|
|
$table = $this->get_table($resource);
|
|
|
|
foreach($attributes as $key => $value) {
|
|
if($this->get_type($resource, $key) === 'json') {
|
|
if($value === null) {
|
|
$attributes[$key] = null;
|
|
}
|
|
else {
|
|
$attributes[$key] = json_encode($value);
|
|
}
|
|
}
|
|
}
|
|
|
|
$attributes = $this->converge_attributes($resource, null, $attributes);
|
|
|
|
$columns = implode(
|
|
',',
|
|
array_map([$this, 'escape_identifier'], array_keys($attributes))
|
|
);
|
|
|
|
$values = implode(
|
|
',',
|
|
array_map([$this, 'escape'], $attributes)
|
|
);
|
|
|
|
$query =
|
|
'insert into ' . $this->escape_identifier($table) .
|
|
'(' . $columns . ') values (' . $values . ')';
|
|
|
|
$this->query($query);
|
|
|
|
if($return_mode === 'row') {
|
|
$read_attributes = [];
|
|
$read_attributes[$table . '_id'] = $this->insert_id;
|
|
return $this->read($resource, $read_attributes)[0];
|
|
} else {
|
|
return $this->insert_id;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the number of queries that have been executed.
|
|
*
|
|
* @return int The query count.
|
|
*/
|
|
public function get_query_count() {
|
|
return $this->query_count;
|
|
}
|
|
|
|
/**
|
|
* Gets the time taken to execute all of the queries.
|
|
*
|
|
* @return float The total execution time.
|
|
*/
|
|
public function get_query_time() {
|
|
return $this->query_time;
|
|
}
|
|
|
|
/**
|
|
* Turn a resource into a table.
|
|
*
|
|
* @param string $resource
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_table($resource) {
|
|
$class_parts = explode('\\', $resource);
|
|
return end($class_parts);
|
|
}
|
|
|
|
/**
|
|
* Get the type of a specific column.
|
|
*
|
|
* @param string $table The table.
|
|
* @param string $column The column.
|
|
*
|
|
* @return string The type.
|
|
*/
|
|
private function get_type($resource, $column) {
|
|
$table = $this->get_table($resource);
|
|
|
|
// If this column is in converged, get the type from there.
|
|
if(
|
|
class_exists($resource) === true && // This will also call the autoloader to make sure it's loaded
|
|
isset($resource::$converged) === true &&
|
|
isset($resource::$converged[$column]) === true
|
|
) {
|
|
return $resource::$converged[$column]['type'];
|
|
}
|
|
|
|
// Otherwise query the entire schema (and cache it) to see what the type is.
|
|
if(isset(self::$types) === false) {
|
|
self::$types = [];
|
|
$result = $this->query('
|
|
select
|
|
`TABLE_NAME`,
|
|
`COLUMN_NAME`,
|
|
`DATA_TYPE`
|
|
from
|
|
`information_schema`.`columns`
|
|
where
|
|
`table_schema` = ' . $this->escape($this->setting->get('database_name')) . '
|
|
');
|
|
while($row = $result->fetch_assoc()) {
|
|
self::$types[$row['TABLE_NAME'] . '.' . $row['COLUMN_NAME']] = $row['DATA_TYPE'];
|
|
}
|
|
}
|
|
|
|
return self::$types[$table . '.' . $column];
|
|
}
|
|
|
|
/**
|
|
* Attempt to get a database lock.
|
|
*
|
|
* @param string $lock_name The lock name.
|
|
* @param int $timeout How long to wait for the lock. Negative values are
|
|
* forever.
|
|
*
|
|
* @throws \Exception If the lock could not be obtained.
|
|
*/
|
|
public function get_lock($lock_name, $timeout = 0) {
|
|
$result = $this->query('
|
|
select
|
|
get_lock(' .
|
|
$this->escape($this->setting->get('database_name') . '_' . $lock_name) . ', ' .
|
|
$this->escape($timeout) .
|
|
') `lock`
|
|
');
|
|
$row = $result->fetch_assoc();
|
|
if($row['lock'] !== 1) {
|
|
throw new exception('Could not get lock.', 1209);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempt to release a database lock.
|
|
*
|
|
* @param string $lock_name The lock name.
|
|
*
|
|
* @throws \Exception If the lock was not established by this thread.
|
|
* @throws \Exception If the lock does not exist.
|
|
*/
|
|
public function release_lock($lock_name) {
|
|
$result = $this->query('
|
|
select
|
|
release_lock(' . $this->escape($this->setting->get('database_name') . '_' . $lock_name) . ') `lock`
|
|
');
|
|
$row = $result->fetch_assoc();
|
|
if($row['lock'] === 0) {
|
|
throw new \Exception('Lock not established by this thread.', 1210);
|
|
}
|
|
else if($row['lock'] === null) {
|
|
throw new \Exception('Lock does not exist.', 1211);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the time zone. Useful for querying in a certain time zone so MySQL
|
|
* will just handle the conversion.
|
|
*
|
|
* @param int $time_zone_offset Offset in minutes.
|
|
*/
|
|
public function set_time_zone($time_zone_offset) {
|
|
$operator = $time_zone_offset < 0 ? '-' : '+';
|
|
$time_zone_offset = abs($time_zone_offset);
|
|
$offset_hours = floor($time_zone_offset / 60);
|
|
$offset_minutes = $time_zone_offset % 60;
|
|
$this->query('SET time_zone = "' . $operator . sprintf('%d', $offset_hours) . ':' . str_pad($offset_minutes, 2, STR_PAD_LEFT) . '"');
|
|
}
|
|
|
|
/**
|
|
* Disable transactions for this connection.
|
|
*/
|
|
public function disable_transactions() {
|
|
$this->use_transactions = false;
|
|
}
|
|
|
|
}
|