table = $table; $this->notORM = $notORM; $this->single = $single; $this->primary = $notORM->structure->getPrimary($table); } /** Save data to cache and empty result */ function __destruct() { if ($this->notORM->cache && !$this->select && isset($this->rows)) { $access = $this->access; if (is_array($access)) { $access = array_filter($access); } $this->notORM->cache->save("$this->table;" . implode(",", $this->conditions), $access); } $this->rows = null; unset($this->data); } protected function limitString($limit, $offset = null) { $return = ""; if (isset($limit) && $this->notORM->driver != "oci" && $this->notORM->driver != "dblib" && $this->notORM->driver != "mssql" && $this->notORM->driver != "sqlsrv") { $return .= " LIMIT $limit"; if ($offset) { $return .= " OFFSET $offset"; } } return $return; } protected function removeExtraDots($expression) { return preg_replace('~(?:\\b[a-z_][a-z0-9_.:]*[.:])?([a-z_][a-z0-9_]*)[.:]([a-z_*])~i', '\\1.\\2', $expression); // rewrite tab1.tab2.col } protected function whereString() { $return = ""; if ($this->group) { $return .= " GROUP BY $this->group"; } if ($this->having) { $return .= " HAVING $this->having"; } if ($this->order) { $return .= " ORDER BY " . implode(", ", $this->order); } $return = $this->removeExtraDots($return); $where = $this->where; if (isset($this->limit) && $this->notORM->driver == "oci") { $where[] = ($where ? " AND " : "") . "(" . ($this->offset ? "rownum > $this->offset AND " : "") . "rownum <= " . ($this->limit + $this->offset) . ")"; //! rownum > doesn't work - requires subselect (see adminer/drivers/oracle.inc.php) } if ($where) { $return = " WHERE " . implode($where) . $return; } $return .= $this->limitString($this->limit, $this->offset); if (isset($this->lock)) { $return .= ($this->lock ? " FOR UPDATE" : " LOCK IN SHARE MODE"); } return $return; } protected function topString($limit, $offset = null) { if (isset($limit) && ($this->notORM->driver == "dblib" || $this->notORM->driver == "mssql" || $this->notORM->driver == "sqlsrv")) { return " TOP ($this->limit)"; //! offset is not supported } return ""; } protected function createJoins($val) { $return = array(); preg_match_all('~\\b([a-z_][a-z0-9_.:]*[.:])[a-z_*]~i', $val, $matches); foreach ($matches[1] as $names) { $parent = $this->table; if ($names != "$parent.") { // case-sensitive preg_match_all('~\\b([a-z_][a-z0-9_]*)([.:])~i', $names, $matches, PREG_SET_ORDER); foreach ($matches as $match) { list(, $name, $delimiter) = $match; $table = $this->notORM->structure->getReferencedTable($name, $parent); $column = ($delimiter == ':' ? $this->notORM->structure->getPrimary($parent) : $this->notORM->structure->getReferencedColumn($name, $parent)); $primary = ($delimiter == ':' ? $this->notORM->structure->getReferencedColumn($parent, $table) : $this->notORM->structure->getPrimary($table)); $return[$name] = " LEFT JOIN $table" . ($table != $name ? " AS $name" : "") . " ON $parent.$column = $name.$primary"; // should use alias if the table is used on more places $parent = $name; } } } return $return; } /** Get SQL query * @return string */ function __toString() { $return = "SELECT" . $this->topString($this->limit, $this->offset) . " "; $join = $this->createJoins(implode(",", $this->conditions) . "," . implode(",", $this->select) . ",$this->group,$this->having," . implode(",", $this->order)); if (!isset($this->rows) && $this->notORM->cache && !is_string($this->accessed)) { $this->accessed = $this->notORM->cache->load("$this->table;" . implode(",", $this->conditions)); $this->access = $this->accessed; } if ($this->select) { $return .= $this->removeExtraDots(implode(", ", $this->select)); } elseif ($this->accessed) { $return .= ($join ? "$this->table." : "") . implode(", " . ($join ? "$this->table." : ""), array_keys($this->accessed)); } else { $return .= ($join ? "$this->table." : "") . "*"; } $return .= " FROM $this->table" . implode($join) . $this->whereString(); if ($this->union) { $return = ($this->notORM->driver == "sqlite" || $this->notORM->driver == "oci" ? $return : "($return)") . implode($this->union); if ($this->unionOrder) { $return .= " ORDER BY " . implode(", ", $this->unionOrder); } $return .= $this->limitString($this->unionLimit, $this->unionOffset); $top = $this->topString($this->unionLimit, $this->unionOffset); if ($top) { $return = "SELECT$top * FROM ($return) t"; } } return $return; } protected function query($query, $parameters) { if ($this->notORM->debug) { if (!is_callable($this->notORM->debug)) { $debug = "$query;"; if ($parameters) { $debug .= " -- " . implode(", ", array_map(array($this, 'quote'), $parameters)); } $pattern = '(^' . preg_quote(dirname(__FILE__)) . '(\\.php$|[/\\\\]))'; // can be static foreach (debug_backtrace() as $backtrace) { if (isset($backtrace["file"]) && !preg_match($pattern, $backtrace["file"])) { // stop on first file outside NotORM source codes break; } } error_log("$backtrace[file]:$backtrace[line]:$debug\n", 0); } elseif (call_user_func($this->notORM->debug, $query, $parameters) === false) { return false; } } $return = $this->notORM->connection->prepare($query); if (!$return || !$return->execute(array_map(array($this, 'formatValue'), $parameters))) { $return = false; } if ($this->notORM->debugTimer) { call_user_func($this->notORM->debugTimer); } return $return; } protected function formatValue($val) { if ($val instanceof DateTime) { return $val->format("Y-m-d H:i:s"); //! may be driver specific } return $val; } protected function quote($val) { if (!isset($val)) { return "NULL"; } if (is_array($val)) { // (a, b) IN ((1, 2), (3, 4)) return "(" . implode(", ", array_map(array($this, 'quote'), $val)) . ")"; } $val = $this->formatValue($val); if (is_float($val)) { return sprintf("%F", $val); // otherwise depends on setlocale() } if ($val === false) { return "0"; } if (is_int($val) || $val instanceof NotORM_Literal) { // number or SQL code - for example "NOW()" return (string) $val; } return $this->notORM->connection->quote($val); } /** Shortcut for call_user_func_array(array($this, 'insert'), $rows) * @param array * @return int number of affected rows or false in case of an error */ function insert_multi(array $rows) { if ($this->notORM->freeze) { return false; } if (!$rows) { return 0; } $data = reset($rows); $parameters = array(); if ($data instanceof NotORM_Result) { $parameters = $data->parameters; //! other parameters $data = (string) $data; } elseif ($data instanceof Traversable) { $data = iterator_to_array($data); } $insert = $data; if (is_array($data)) { $values = array(); foreach ($rows as $value) { if ($value instanceof Traversable) { $value = iterator_to_array($value); } $values[] = $this->quote($value); foreach ($value as $val) { if ($val instanceof NotORM_Literal && $val->parameters) { $parameters = array_merge($parameters, $val->parameters); } } } //! driver specific extended insert $insert = ($data || $this->notORM->driver == "mysql" ? "(" . implode(", ", array_keys($data)) . ") VALUES " . implode(", ", $values) : "DEFAULT VALUES" ); } // requires empty $this->parameters $return = $this->query("INSERT INTO $this->table $insert", $parameters); if (!$return) { return false; } $this->rows = null; return $return->rowCount(); } /** Insert row in a table * @param mixed array($column => $value)|Traversable for single row insert or NotORM_Result|string for INSERT ... SELECT * @param ... used for extended insert * @return mixed inserted NotORM_Row or false in case of an error or number of affected rows for INSERT ... SELECT */ function insert($data) { $rows = func_get_args(); $return = $this->insert_multi($rows); if (!$return) { return false; } if (!is_array($data)) { return $return; } if (!isset($data[$this->primary]) && ($id = $this->notORM->connection->lastInsertId($this->notORM->structure->getSequence($this->table)))) { $data[$this->primary] = $id; } return new $this->notORM->rowClass($data, $this); } /** Update all rows in result set * @param array ($column => $value) * @return int number of affected rows or false in case of an error */ function update(array $data) { if ($this->notORM->freeze) { return false; } if (!$data) { return 0; } $values = array(); $parameters = array(); foreach ($data as $key => $val) { // doesn't use binding because $this->parameters can be filled by ? or :name $values[] = "$key = " . $this->quote($val); if ($val instanceof NotORM_Literal && $val->parameters) { $parameters = array_merge($parameters, $val->parameters); } } if ($this->parameters) { $parameters = array_merge($parameters, $this->parameters); } // joins in UPDATE are supported only in MySQL $return = $this->query("UPDATE" . $this->topString($this->limit, $this->offset) . " $this->table SET " . implode(", ", $values) . $this->whereString(), $parameters); if (!$return) { return false; } return $return->rowCount(); } /** Insert row or update if it already exists * @param array ($column => $value) * @param array ($column => $value) * @param array ($column => $value), empty array means use $insert * @return int number of affected rows or false in case of an error */ function insert_update(array $unique, array $insert, array $update = array()) { if (!$update) { $update = $insert; } $insert = $unique + $insert; $values = "(" . implode(", ", array_keys($insert)) . ") VALUES " . $this->quote($insert); //! parameters if ($this->notORM->driver == "mysql") { $set = array(); if (!$update) { $update = $unique; } foreach ($update as $key => $val) { $set[] = "$key = " . $this->quote($val); //! parameters } return $this->insert("$values ON DUPLICATE KEY UPDATE " . implode(", ", $set)); } else { $connection = $this->notORM->connection; $errorMode = $connection->getAttribute(PDO::ATTR_ERRMODE); $connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); try { $return = $this->insert($values); $connection->setAttribute(PDO::ATTR_ERRMODE, $errorMode); return $return; } catch (PDOException $e) { $connection->setAttribute(PDO::ATTR_ERRMODE, $errorMode); if ($e->getCode() == "23000" || $e->getCode() == "23505") { // "23000" - duplicate key, "23505" unique constraint pgsql if (!$update) { return 0; } $clone = clone $this; $return = $clone->where($unique)->update($update); return ($return ? $return + 1 : $return); } if ($errorMode == PDO::ERRMODE_EXCEPTION) { throw $e; } elseif ($errorMode == PDO::ERRMODE_WARNING) { trigger_error("PDOStatement::execute(): " . $e->getMessage(), E_USER_WARNING); // E_WARNING is unusable } } } } /** Get last insert ID * @return string number */ function insert_id() { return $this->notORM->connection->lastInsertId(); } /** Delete all rows in result set * @return int number of affected rows or false in case of an error */ function delete() { if ($this->notORM->freeze) { return false; } $return = $this->query("DELETE" . $this->topString($this->limit, $this->offset) . " FROM $this->table" . $this->whereString(), $this->parameters); if (!$return) { return false; } return $return->rowCount(); } /** Add select clause, more calls appends to the end * @param string for example "column, MD5(column) AS column_md5", empty string to reset previously set columns * @param string ... * @return NotORM_Result fluent interface */ function select($columns) { $this->__destruct(); if ($columns != "") { foreach (func_get_args() as $columns) { $this->select[] = $columns; } } else { $this->select = array(); } return $this; } /** Add where condition, more calls appends with AND * @param mixed string possibly containing ? or :name; or array($condition => $parameters, ...) * @param mixed array accepted by PDOStatement::execute or a scalar value * @param mixed ... * @return NotORM_Result fluent interface */ function where($condition, $parameters = array()) { $args = func_get_args(); return $this->whereOperator("AND", $args); } protected function whereOperator($operator, array $args) { $condition = $args[0]; $parameters = (count($args) > 1 ? $args[1] : array()); if (is_array($condition)) { // where(array("column1" => 1, "column2 > ?" => 2)) foreach ($condition as $key => $val) { $this->where($key, $val); } return $this; } $this->__destruct(); $this->conditions[] = "$operator $condition"; $condition = $this->removeExtraDots($condition); if (count($args) != 2 || strpbrk($condition, "?:")) { // where("column < ? OR column > ?", array(1, 2)) if (count($args) != 2 || !is_array($parameters)) { // where("column < ? OR column > ?", 1, 2) $parameters = array_slice($args, 1); } $this->parameters = array_merge($this->parameters, $parameters); } elseif ($parameters === null) { // where("column", null) $condition .= " IS NULL"; } elseif ($parameters instanceof NotORM_Result) { // where("column", $db->$table()) $clone = clone $parameters; if (!$clone->select) { $clone->select($this->notORM->structure->getPrimary($clone->table)); } if ($this->notORM->driver != "mysql") { if ($clone instanceof NotORM_MultiResult) { array_shift($clone->select); $clone->single(); } $condition .= " IN ($clone)"; $this->parameters = array_merge($this->parameters, $clone->parameters); } else { $in = array(); foreach ($clone as $row) { $row = array_values(iterator_to_array($row)); if ($clone instanceof NotORM_MultiResult && count($row) > 1) { array_shift($row); } if (count($row) == 1) { $in[] = $this->quote($row[0]); } else { $in[] = $this->quote($row); } } if ($in) { $condition .= " IN (" . implode(", ", $in) . ")"; } else { $condition = "($condition) IS NOT NULL AND $condition IS NULL"; // $condition = "NOT id" } } } elseif (!is_array($parameters)) { // where("column", "x") $condition .= " = " . $this->quote($parameters); } else { // where("column", array(1, 2)) $condition = $this->whereIn($condition, $parameters); } $this->where[] = (preg_match('~^\)+$~', $condition) ? $condition : ($this->where ? " $operator " : "") . "($condition)" ); return $this; } protected function whereIn($condition, $parameters) { if (!$parameters) { $condition = "($condition) IS NOT NULL AND $condition IS NULL"; } elseif ($this->notORM->driver != "oci") { $column = $condition; $condition .= " IN " . $this->quote($parameters); $nulls = array_filter($parameters, 'is_null'); if ($nulls) { $condition = "$condition OR $column IS NULL"; } } else { // http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/expressions014.htm $or = array(); for ($i=0; $i < count($parameters); $i += 1000) { $or[] = "$condition IN " . $this->quote(array_slice($parameters, $i, 1000)); } $condition = implode(" OR ", $or); } return $condition; } function __call($name, array $args) { $operator = strtoupper($name); switch ($operator) { case "AND": case "OR": return $this->whereOperator($operator, $args); } trigger_error("Call to undefined method NotORM_Result::$name()", E_USER_ERROR); } /** Shortcut for where() * @param string * @param mixed * @param mixed ... * @return NotORM_Result fluent interface */ function __invoke($where, $parameters = array()) { $args = func_get_args(); return $this->whereOperator("AND", $args); } /** Add order clause, more calls appends to the end * @param mixed "column1, column2 DESC" or array("column1", "column2 DESC"), empty string to reset previous order * @param string ... * @return NotORM_Result fluent interface */ function order($columns) { $this->rows = null; if ($columns != "") { $columns = (is_array($columns) ? $columns : func_get_args()); foreach ($columns as $column) { if ($this->union) { $this->unionOrder[] = $column; } else { $this->order[] = $column; } } } elseif ($this->union) { $this->unionOrder = array(); } else { $this->order = array(); } return $this; } /** Set limit clause, more calls rewrite old values * @param int * @param int * @return NotORM_Result fluent interface */ function limit($limit, $offset = null) { $this->rows = null; if ($this->union) { $this->unionLimit = +$limit; $this->unionOffset = +$offset; } else { $this->limit = +$limit; $this->offset = +$offset; } return $this; } /** Set group clause, more calls rewrite old values * @param string * @param string * @return NotORM_Result fluent interface */ function group($columns, $having = "") { $this->__destruct(); $this->group = $columns; $this->having = $having; return $this; } /** Set select FOR UPDATE or LOCK IN SHARE MODE * @param bool * @return NotORM_Result fluent interface */ function lock($exclusive = true) { $this->lock = $exclusive; return $this; } /** * @param NotORM_Result * @param bool * @return NotORM_Result fluent interface */ function union(NotORM_Result $result, $all = false) { $this->union[] = " UNION " . ($all ? "ALL " : "") . ($this->notORM->driver == "sqlite" || $this->notORM->driver == "oci" ? $result : "($result)"); $this->parameters = array_merge($this->parameters, $result->parameters); return $this; } /** Execute aggregation function * @param string * @return string */ function aggregation($function) { $join = $this->createJoins(implode(",", $this->conditions) . ",$function"); $query = "SELECT $function FROM $this->table" . implode($join); if ($this->where) { $query .= " WHERE " . implode($this->where); } foreach ($this->query($query, $this->parameters)->fetch() as $return) { return $return; } } /** Count number of rows * @param string * @return int */ function count($column = "") { if (!$column) { $this->execute(); return count($this->data); } return $this->aggregation("COUNT($column)"); } /** Return minimum value from a column * @param string * @return int */ function min($column) { return $this->aggregation("MIN($column)"); } /** Return maximum value from a column * @param string * @return int */ function max($column) { return $this->aggregation("MAX($column)"); } /** Return sum of values in a column * @param string * @return int */ function sum($column) { return $this->aggregation("SUM($column)"); } /** Execute the built query * @return null */ protected function execute() { if (!isset($this->rows)) { $result = false; $exception = null; $parameters = array(); foreach (array_merge($this->select, array($this, $this->group, $this->having), $this->order, $this->unionOrder) as $val) { if (($val instanceof NotORM_Literal || $val instanceof self) && $val->parameters) { $parameters = array_merge($parameters, $val->parameters); } } try { $result = $this->query($this->__toString(), $parameters); } catch (PDOException $exception) { // handled later } if (!$result) { if (!$this->select && $this->accessed) { $this->accessed = ''; $this->access = array(); $result = $this->query($this->__toString(), $parameters); } elseif ($exception) { throw $exception; } } $this->rows = array(); if ($result) { $result->setFetchMode(PDO::FETCH_ASSOC); foreach ($result as $key => $row) { if (isset($row[$this->primary])) { $key = $row[$this->primary]; if (!is_string($this->access)) { $this->access[$this->primary] = true; } } $this->rows[$key] = new $this->notORM->rowClass($row, $this); } } $this->data = $this->rows; } } /** Fetch next row of result * @param string column name to return or an empty string for the whole row * @return mixed string or null with $column, NotORM_Row without $column, false if there is no row */ function fetch($column = '') { // no $this->select($column) because next calls can access different columns $this->execute(); $return = current($this->data); next($this->data); if ($return && $column != '') { return $return[$column]; } return $return; } /** Fetch all rows as associative array * @param string * @param string column name used for an array value or an empty string for the whole row * @return array */ function fetchPairs($key, $value = '') { $return = array(); $clone = clone $this; if ($value != "") { $clone->select = array(); $clone->select("$key, $value"); // MultiResult adds its column } elseif ($clone->select) { array_unshift($clone->select, $key); } else { $clone->select = array("$key, $this->table.*"); } foreach ($clone as $row) { $values = array_values(iterator_to_array($row)); if ($value != "" && $clone instanceof NotORM_MultiResult) { array_shift($values); } $return[(string) $values[0]] = ($value != "" ? $values[(array_key_exists(1, $values) ? 1 : 0)] : $row); // isset($values[1]) - fetchPairs("id", "id") } return $return; } protected function access($key, $delete = false) { if ($delete) { if (is_array($this->access)) { $this->access[$key] = false; } return false; } if (!isset($key)) { $this->access = ''; } elseif (!is_string($this->access)) { $this->access[$key] = true; } if (!$this->select && $this->accessed && (!isset($key) || !isset($this->accessed[$key]))) { $this->accessed = ''; $this->rows = null; return true; } return false; } protected function single() { } // Iterator implementation (not IteratorAggregate because $this->data can be changed during iteration) function rewind() { $this->execute(); $this->keys = array_keys($this->data); reset($this->keys); } /** @return NotORM_Row */ function current() { return $this->data[current($this->keys)]; } /** @return string row ID */ function key() { return current($this->keys); } function next() { next($this->keys); } function valid() { return current($this->keys) !== false; } // ArrayAccess implementation /** Test if row exists * @param string row ID or array for where conditions * @return bool */ function offsetExists($key) { $row = $this->offsetGet($key); return isset($row); } /** Get specified row * @param string row ID or array for where conditions * @return NotORM_Row or null if there is no such row */ function offsetGet($key) { if ($this->single && !isset($this->data)) { $clone = clone $this; if (is_array($key)) { $clone->where($key)->limit(1); } else { $clone->where($this->primary, $key); } $return = $clone->fetch(); if ($return) { return $return; } } else { $this->execute(); if (is_array($key)) { foreach ($this->data as $row) { foreach ($key as $k => $v) { if ((isset($v) && $row[$k] !== null ? $row[$k] != $v : $row[$k] !== $v)) { continue 2; } } return $row; } } elseif (isset($this->data[$key])) { return $this->data[$key]; } } } /** Mimic row * @param string row ID * @param NotORM_Row * @return null */ function offsetSet($key, $value) { $this->execute(); $this->data[$key] = $value; } /** Remove row from result set * @param string row ID * @return null */ function offsetUnset($key) { $this->execute(); unset($this->data[$key]); } // JsonSerializable implementation function jsonSerialize() { $this->execute(); if ($this->notORM->jsonAsArray) { return array_values($this->data); } else { return $this->data; } } }