diff --git a/app/lib/db/cortex.php b/app/lib/db/cortex.php index 18120ab8..c922eb34 100644 --- a/app/lib/db/cortex.php +++ b/app/lib/db/cortex.php @@ -1,7 +1,7 @@ * https://github.com/ikkez/F3-Sugar/ * * @package DB - * @version 1.4.2-dev - * @date 29.01.2016 + * @version 1.5.0-dev + * @date 27.02.2017 * @since 24.04.2012 */ @@ -53,7 +53,8 @@ class Cortex extends Cursor { $countFields, // relational counter buffer $preBinds, // bind values to be prepended to $filter $vFields, // virtual fields buffer - $_ttl; // rel_ttl overwrite + $_ttl, // rel_ttl overwrite + $charset; // sql collation charset /** @var Cursor */ protected $mapper; @@ -61,8 +62,11 @@ class Cortex extends Cursor { /** @var CortexQueryParser */ protected $queryParser; - static - $init = false; // just init without mapper + /** @var bool initialization flag */ + static $init = false; + + /** @var array sql table schema cache */ + static $schema_cache = []; const // special datatypes @@ -113,10 +117,10 @@ class Cortex extends Cursor { $this->table = $this->getTable(); if (!$this->table) trigger_error(self::E_NO_TABLE,E_USER_ERROR); - $this->ttl = $ttl ?: 60; + $this->ttl = $ttl ?: ($this->ttl ?: 60); if (!$this->rel_ttl) $this->rel_ttl = 0; - $this->_ttl = $this->rel_ttl ?: 0; + $this->_ttl = $this->rel_ttl ?: ($this->rel_ttl ?: 0); if (static::$init == TRUE) return; if ($this->fluid) static::setup($this->db,$this->table,array()); @@ -132,8 +136,10 @@ class Cortex extends Cursor { $this->mapper = new Jig\Mapper($this->db, $this->table); break; case 'sql': - $this->mapper = new SQL\Mapper($this->db, $this->table, $this->whitelist, + // ensure to load full table schema, so we can work with it at runtime + $this->mapper = new SQL\Mapper($this->db, $this->table, null, ($this->fluid)?0:$this->ttl); + $this->applyWhitelist(); break; case 'mongo': $this->mapper = new Mongo\Mapper($this->db, $this->table); @@ -163,23 +169,33 @@ class Cortex extends Cursor { * @return array */ public function fields(array $fields=array(), $exclude=false) { + $addInc=[]; if ($fields) - // collect restricted fields for related mappers + // collect & set restricted fields for related mappers foreach($fields as $i=>$val) if(is_int(strpos($val,'.'))) { list($key, $relField) = explode('.',$val,2); $this->relWhitelist[$key][(int)$exclude][] = $relField; unset($fields[$i]); - $fields[] = $key; + $addInc[] = $key; } $fields = array_unique($fields); $schema = $this->whitelist ?: $this->mapper->fields(); - if (!$schema && !$this->dbsType != 'sql' && $this->dry()) { + if (!$schema && $this->dbsType != 'sql' && $this->dry()) { $schema = $this->load()->mapper->fields(); $this->reset(); } + // include relation linkage fields to $fields (if $fields is a whitelist) + if (!$exclude && !empty($fields) && !empty($addInc)) + $fields=array_unique(array_merge($fields,$addInc)); + // include relation linkage fields to existing whitelist (if $fields is a blacklist or there's nothing else to whitelist) + elseif (!empty($addInc) && $this->whitelist) + $this->whitelist=array_unique(array_merge($this->whitelist,$addInc)); + // initially merge configured fields into schema (add virtual/rel fields to schema) if (!$this->whitelist && $this->fieldConf) - $schema=array_unique(array_merge($schema,array_keys($this->fieldConf))); + $schema=array_unique(array_merge($schema, + array_keys($this->fieldConf),array_keys($this->vFields?:[]))); + // skip if there's nothing to set for own model if (!$fields || empty($fields)) return $schema; elseif ($exclude) { @@ -187,12 +203,32 @@ class Cortex extends Cursor { } else $this->whitelist=$fields; $id=$this->dbsType=='sql'?$this->primary:'_id'; - if(!in_array($id,$this->whitelist)) + if (!in_array($id,$this->whitelist)) $this->whitelist[]=$id; - $this->initMapper(); + $this->applyWhitelist(); return $this->whitelist; } + /** + * apply whitelist to active mapper schema + */ + protected function applyWhitelist() { + if ($this->dbsType == 'sql') { + // fetch full schema + if (!$this->fluid && isset(self::$schema_cache[$this->table])) + $schema = self::$schema_cache[$this->table]; + else { + $schema = $this->mapper->schema(); + self::$schema_cache[$this->table] = $schema; + } + // apply reduced fields schema + if ($this->whitelist) + $schema = array_intersect_key($schema, array_flip($this->whitelist)); + $this->mapper->schema($schema); + $this->mapper->reset(); + } + } + /** * set model definition * config example: @@ -232,6 +268,7 @@ class Cortex extends Cursor { 'db'=>$self->db, 'fluid'=>$self->fluid, 'primary'=>$self->primary, + 'charset'=>$self->charset, ); unset($self); return $conf; @@ -292,54 +329,55 @@ class Cortex extends Cursor { if ($db instanceof SQL) { $schema = new Schema($db); // prepare field configuration - if (!empty($fields)) - foreach($fields as $key => &$field) { - // fetch relation field types - $field = static::resolveRelationConf($field); - // check m:m relation - if (array_key_exists('has-many', $field)) { - // m:m relation conf [class,to-key,from-key] - if (is_array($relConf = $field['has-many'])) { - $rel = $relConf[0]::resolveConfiguration(); - // check if foreign conf matches m:m - if (array_key_exists($relConf[1],$rel['fieldConf']) - && !is_null($rel['fieldConf'][$relConf[1]]) - && $relConf['hasRel'] == 'has-many') { - // compute mm table name - $mmTable = isset($relConf[2]) ? $relConf[2] : - static::getMMTableName($rel['table'], $relConf['relField'], - $table, $key, $rel['fieldConf'][$relConf[1]]['has-many']); - if (!in_array($mmTable,$schema->getTables())) { - $mmt = $schema->createTable($mmTable); - $relField = $relConf['relField'].($relConf['isSelf']?'_ref':''); - $mmt->addColumn($relField)->type($relConf['relFieldType']); - $mmt->addColumn($key)->type($field['type']); - $index = array($relField,$key); - sort($index); - $mmt->addIndex($index); - $mmt->build(); - } + foreach($fields as $key => &$field) { + // fetch relation field types + $field = static::resolveRelationConf($field); + // check m:m relation + if (array_key_exists('has-many', $field)) { + // m:m relation conf [class,to-key,from-key] + if (is_array($relConf = $field['has-many'])) { + $rel = $relConf[0]::resolveConfiguration(); + // check if foreign conf matches m:m + if (array_key_exists($relConf[1],$rel['fieldConf']) + && !is_null($rel['fieldConf'][$relConf[1]]) + && $relConf['hasRel'] == 'has-many') { + // compute mm table name + $mmTable = isset($relConf[2]) ? $relConf[2] : + static::getMMTableName($rel['table'], $relConf['relField'], + $table, $key, $rel['fieldConf'][$relConf[1]]['has-many']); + if (!in_array($mmTable,$schema->getTables())) { + $mmt = $schema->createTable($mmTable); + $relField = $relConf['relField'].($relConf['isSelf']?'_ref':''); + $mmt->addColumn($relField)->type($relConf['relFieldType']); + $mmt->addColumn($key)->type($field['type']); + $index = array($relField,$key); + sort($index); + $mmt->addIndex($index); + $mmt->build(); } } - unset($fields[$key]); - continue; } - // skip virtual fields with no type - if (!array_key_exists('type', $field)) { - unset($fields[$key]); - continue; - } - // transform array fields - if (in_array($field['type'], array(self::DT_JSON, self::DT_SERIALIZED))) - $field['type']=$schema::DT_TEXT; - // defaults values - if (!array_key_exists('nullable', $field)) - $field['nullable'] = true; - unset($field); + unset($fields[$key]); + continue; } + // skip virtual fields with no type + if (!array_key_exists('type', $field)) { + unset($fields[$key]); + continue; + } + // transform array fields + if (in_array($field['type'], array(self::DT_JSON, self::DT_SERIALIZED))) + $field['type']=$schema::DT_TEXT; + // defaults values + if (!array_key_exists('nullable', $field)) + $field['nullable'] = true; + unset($field); + } if (!in_array($table, $schema->getTables())) { // create table $table = $schema->createTable($table); + if (isset($df) && $df['charset']) + $table->setCharset($df['charset']); foreach ($fields as $field_key => $field_conf) $table->addColumn($field_key, $field_conf); if(isset($df) && $df['primary'] != 'id') { @@ -444,7 +482,8 @@ class Cortex extends Cursor { $mmTable = array($ftable.'__'.$fkey, $ptable.'__'.$pkey); natcasesort($mmTable); // shortcut for self-referencing mm tables - if ($mmTable[0] == $mmTable[1] || ($fConf && $fConf['isSelf']==true)) + if ($mmTable[0] == $mmTable[1] || + ($fConf && isset($fConf['isSelf']) && $fConf['isSelf']==true)) return array_shift($mmTable); $return = strtolower(str_replace('\\', '_', implode('_mm_', $mmTable))); return $return; @@ -517,8 +556,8 @@ class Cortex extends Cursor { // has-many <> belongs-to-one (m:1) $field['has-many']['hasRel'] = 'belongs-to-one'; $toConf=$rel['fieldConf'][$relConf[1]]['belongs-to-one']; - if (is_array($toConf)) - $field['has-many']['relField'] = $toConf[1]; + $field['has-many']['relField'] = is_array($toConf) ? + $toConf[1] : $rel['primary']; } } elseif(array_key_exists('has-one', $field)) $field['relType'] = 'has-one'; @@ -548,13 +587,12 @@ class Cortex extends Cursor { public function find($filter = NULL, array $options = NULL, $ttl = 0) { $sort=false; if ($this->dbsType!='sql') { - if (!empty($this->countFields)) - // see if reordering is needed - foreach($this->countFields as $counter) { - if ($options && isset($options['order']) && - preg_match('/count_'.$counter.'\h+(asc|desc)/i',$options['order'],$match)) - $sort=true; - } + // see if reordering is needed + foreach($this->countFields?:[] as $counter) { + if ($options && isset($options['order']) && + preg_match('/count_'.$counter.'\h+(asc|desc)/i',$options['order'],$match)) + $sort=true; + } if ($sort) { // backup slice settings if (isset($options['limit'])) { @@ -575,14 +613,13 @@ class Cortex extends Cursor { $record = $this->factory($record); unset($record); } - if (!empty($this->countFields)) - // add counter for NoSQL engines - foreach($this->countFields as $counter) - foreach($result as &$mapper) { - $cr=$mapper->get($counter); - $mapper->virtual('count_'.$counter,$cr?count($cr):null); - unset($mapper); - } + // add counter for NoSQL engines + foreach($this->countFields?:[] as $counter) + foreach($result as &$mapper) { + $cr=$mapper->get($counter); + $mapper->virtual('count_'.$counter,$cr?count($cr):null); + unset($mapper); + } $cc = new CortexCollection(); $cc->setModels($result); if($sort) { @@ -599,7 +636,7 @@ class Cortex extends Cursor { * @param array $options * @param int $ttl * @param bool $count - * @return array|false array of underlying cursor objects + * @return array|int|false array of underlying cursor objects */ protected function filteredFind($filter = NULL, array $options = NULL, $ttl = 0, $count=false) { if ($this->grp_stack) { @@ -704,7 +741,7 @@ class Cortex extends Cursor { $filter[0] .= ' and '; $cond = array_shift($addToFilter); if ($this->dbsType=='sql') - $cond = $this->_sql_prependTableToFields($cond,$this->table); + $cond = $this->queryParser->sql_prependTableToFields($cond,$this->table); $filter[0] .= '('.$cond.')'; $filter = array_merge($filter, $addToFilter); } @@ -717,31 +754,39 @@ class Cortex extends Cursor { if (isset($options['order']) && $this->db->driver() == 'pgsql') // PostgreSQLism: sort NULL values to the end of a table $options['order'] = preg_replace('/\h+DESC/i',' DESC NULLS LAST',$options['order']); - if (!empty($hasJoin)) { + if ($hasJoin) { // assemble full sql query $adhoc=''; if ($count) $sql = 'SELECT COUNT(*) AS '.$this->db->quotekey('rows').' FROM '.$qtable; else { - if (!empty($this->preBinds)) { + if ($this->preBinds) { $crit = array_shift($filter); $filter = array_merge($this->preBinds,$filter); array_unshift($filter,$crit); } if (!empty($m_refl_adhoc)) foreach ($m_refl_adhoc as $key=>$val) - $adhoc.=', '.$val['expr'].' AS '.$key; + $adhoc.=', '.$val['expr'].' AS '.$this->db->quotekey($key); $sql = 'SELECT '.$qtable.'.*'.$adhoc.' FROM '.$qtable; } $sql .= ' '.implode(' ',$hasJoin).' WHERE '.$filter[0]; if (!$count) { $db=$this->db; if (isset($options['group'])) - $sql.=' GROUP BY '.preg_replace_callback('/\w+[._\-\w]*/i', function($match) use($db) { - return $db->quotekey($match[0]); - }, $options['group']); + $sql.=' GROUP BY '.preg_replace_callback('/\w+[._\-\w]*/i', + function($match) use($db) { + return $db->quotekey($match[0]); + }, $options['group']); if (isset($options['order'])) - $sql .= ' ORDER BY '.$options['order']; + $sql.=' ORDER BY '.implode(',',array_map( + function($str) use($db) { + return preg_match('/^\h*(\w+[._\-\w]*)(?:\h+((?:ASC|DESC)[\w\h]*))?\h*$/i', + $str,$parts)? + ($db->quotekey($parts[1]). + (isset($parts[2])?(' '.$parts[2]):'')):$str; + }, + explode(',',$options['order']))); if (preg_match('/mssql|sqlsrv|odbc/', $this->db->driver()) && (isset($options['limit']) || isset($options['offset']))) { $ofs=isset($options['offset'])?(int)$options['offset']:0; @@ -781,7 +826,7 @@ class Cortex extends Cursor { unset($record, $mapper); } return $result; - } elseif (!empty($this->preBinds) && !$count) { + } elseif (!empty($this->preBinds)) { // bind values to adhoc queries if (!$filter) // we (PDO) need any filter to bind values @@ -792,7 +837,7 @@ class Cortex extends Cursor { } } return ($count) - ? $this->mapper->count($filter, [], $ttl) + ? $this->mapper->count($filter,$options,$ttl) : $this->mapper->find($filter,$this->queryParser->prepareOptions($options,$this->dbsType),$ttl); } @@ -932,8 +977,9 @@ class Cortex extends Cursor { $rel = $this->getRelInstance($relModel,null,$key); $fkey = is_array($this->fieldConf[$key]['belongs-to-one']) ? $this->fieldConf[$key]['belongs-to-one'][1] : $rel->primary; - $query = $this->_sql_left_join($key,$this->table,$fkey,$table); - $this->_sql_mergeRelCondition($cond,$table,$filter,$options); + $alias = $table.'__'.$key; + $query = $this->_sql_left_join($key,$this->table,$fkey,[$table,$alias]); + $this->_sql_mergeRelCondition($cond,$alias,$filter,$options); return $query; } @@ -970,11 +1016,11 @@ class Cortex extends Cursor { protected function _sql_mergeRelCondition($cond, $table, &$filter, &$options, $glue='AND') { if (!empty($cond[0])) { $whereClause = '('.array_shift($cond[0]).')'; - $whereClause = $this->_sql_prependTableToFields($whereClause,$table); + $whereClause = $this->queryParser->sql_prependTableToFields($whereClause,$table); if (!$filter) $filter = array($whereClause); elseif (!empty($filter[0])) - $filter[0] = '('.$this->_sql_prependTableToFields($filter[0],$this->table) + $filter[0] = '('.$this->queryParser->sql_prependTableToFields($filter[0],$this->table) .') '.$glue.' '.$whereClause; $filter = array_merge($filter, $cond[0]); } @@ -984,24 +1030,6 @@ class Cortex extends Cursor { } } - /** - * add table prefix to identifiers which do not have a table prefix yet - * @param string $cond - * @param string $table - * @return string - */ - protected function _sql_prependTableToFields($cond, $table) { - return preg_replace_callback('/(\w+\((?:[^)(]+|(?R))*\))|'. - '(?:(\s)|^|(?<=[(]))([a-zA-Z_](?:[\w\-_]+))(?=[\s<>=!)]|$)/i', - function($match) use($table) { - if (!isset($match[3])) - return $match[1]; - if (preg_match('/\b(AND|OR|IN|LIKE|NOT)\b/i',$match[3])) - return $match[0]; - return $match[2].$table.'.'.$match[3]; - }, $cond); - } - /** * add filter for loading related models * @param string $key @@ -1165,14 +1193,15 @@ class Cortex extends Cursor { } /** + * Count records that match criteria * @param null $filter - * @param array|NULL $options + * @param array $options * @param int $ttl - * @return array|false + * @return mixed */ - public function count($filter = NULL, array $options=NULL, $ttl = 60) { + public function count($filter=NULL, array $options=NULL, $ttl=60) { $has=$this->hasCond; - $count=$this->filteredFind($filter,null,$ttl,true); + $count=$this->filteredFind($filter,$options,$ttl,true); $this->hasCond=$has; return $count; } @@ -1189,18 +1218,27 @@ class Cortex extends Cursor { * add a virtual field that counts occurring relations * @param $key */ - public function countRel($key) { + public function countRel($key, $alias=null, $filter=null, $option=null) { + if (!$alias) + $alias = 'count_'.$key; + $filter_bak = null; + if ($filter || $option) { + $filter_bak = isset($this->relFilter[$key]) ? $this->relFilter[$key] : false; + $this->filter($key,$filter,$option); + } if (isset($this->fieldConf[$key])){ // one-to-one, one-to-many if ($this->fieldConf[$key]['relType'] == 'belongs-to-one') { if ($this->dbsType == 'sql') { - $this->mapper->set('count_'.$key,'count('.$key.')'); + $this->mapper->set($alias,'count('.$this->db->quotekey($key).')'); $this->grp_stack=(!$this->grp_stack)?$key:$this->grp_stack.','.$key; + if ($this->whitelist && !in_array($alias,$this->whitelist)) + $this->whitelist[] = $alias; } elseif ($this->dbsType == 'mongo') $this->_mongo_addGroup(array( 'keys'=>array($key=>1), - 'reduce' => 'prev.count_'.$key.'++;', - "initial" => array("count_".$key => 0) + 'reduce' => 'prev.'.$alias.'++;', + "initial" => array($alias => 0) )); else trigger_error('Cannot add direct relational counter.',E_USER_ERROR); @@ -1216,16 +1254,23 @@ class Cortex extends Cursor { if (array_key_exists($key, $this->relFilter) && !empty($this->relFilter[$key][0])) { $options=array(); - $from = $mmTable.' '.$this->_sql_left_join($key,$mmTable,$relConf['relPK'],$relConf['relTable']); + $from = $mmTable.' '.$this->_sql_left_join($key,$mmTable, + $relConf['relPK'],$relConf['relTable']); $relFilter = $this->relFilter[$key]; - $this->_sql_mergeRelCondition($relFilter,$relConf['relTable'],$filter,$options); + $this->_sql_mergeRelCondition($relFilter,$relConf['relTable'], + $filter,$options); } - $filter = $this->queryParser->prepareFilter($filter, $this->dbsType, $this->db, $this->fieldConf); + $filter = $this->queryParser->prepareFilter($filter, + $this->dbsType, $this->db, $this->fieldConf); $crit = array_shift($filter); if (count($filter)>0) - $this->preBinds+=$filter; - $this->mapper->set('count_'.$key,'(select count('.$mmTable.'.'.$relConf['relField'].') from '.$from. - ' where '.$crit.' group by '.$mmTable.'.'.$relConf['relField'].')'); + $this->preBinds=array_merge($this->preBinds,$filter); + $this->mapper->set($alias, + '(select count('.$this->db->quotekey($mmTable.'.'.$relConf['relField']).') from '. + $this->db->quotekey($from).' where '.$crit. + ' group by '.$this->db->quotekey($mmTable.'.'.$relConf['relField']).')'); + if ($this->whitelist && !in_array($alias,$this->whitelist)) + $this->whitelist[] = $alias; } else { // count rel $this->countFields[]=$key; @@ -1235,15 +1280,22 @@ class Cortex extends Cursor { if ($this->dbsType == 'sql') { $fConf=$relConf[0]::resolveConfiguration(); $fTable=$fConf['table']; + $fAlias=$fTable.'__count'; $rKey=$relConf[1]; - $crit = $fTable.'.'.$rKey.' = '.$this->table.'.'.$this->primary; + $crit = $fAlias.'.'.$rKey.' = '.$this->table.'.'.$relConf['relField']; $filter = $this->mergeWithRelFilter($key,array($crit)); - $filter = $this->queryParser->prepareFilter($filter, $this->dbsType, $this->db, $this->fieldConf); + $filter[0] = $this->queryParser->sql_prependTableToFields($filter[0],$fAlias); + $filter = $this->queryParser->prepareFilter($filter, + $this->dbsType, $this->db, $this->fieldConf); $crit = array_shift($filter); if (count($filter)>0) - $this->preBinds+=$filter; - $this->mapper->set('count_'.$key,'(select count('.$fTable.'.'.$fConf['primary'].') from '.$fTable.' where '. - $crit.' group by '.$fTable.'.'.$rKey.')'); + $this->preBinds=array_merge($this->preBinds,$filter); + $this->mapper->set($alias, + '(select count('.$this->db->quotekey($fAlias.'.'.$fConf['primary']).') from '. + $this->db->quotekey($fTable).' AS '.$this->db->quotekey($fAlias).' where '. + $crit.' group by '.$this->db->quotekey($fAlias.'.'.$rKey).')'); + if ($this->whitelist && !in_array($alias,$this->whitelist)) + $this->whitelist[] = $alias; } else { // count rel $this->countFields[]=$key; @@ -1251,6 +1303,12 @@ class Cortex extends Cursor { } } } + if ($filter_bak!==null) { + if ($filter_bak) + $this->relFilter[$key] = $filter_bak; + else + $this->clearFilter($key); + } } /** @@ -1405,12 +1463,9 @@ class Cortex extends Cursor { // update mapper fields $newField = $table->getCols(true); $newField = $newField[$key]; - $refl = new \ReflectionObject($this->mapper); - $prop = $refl->getProperty('fields'); - $prop->setAccessible(true); - $fields = $prop->getValue($this->mapper); + $fields = $this->mapper->schema(); $fields[$key] = $newField + array('value'=>NULL,'initial'=>NULL,'changed'=>NULL); - $prop->setValue($this->mapper,$fields); + $this->mapper->schema($fields); } } // custom setter @@ -1465,6 +1520,10 @@ class Cortex extends Cursor { */ public function virtual($key, $val) { $this->vFields[$key]=$val; + if (!empty($this->whitelist)) { + $this->whitelist[] = $key; + $this->whitelist = array_unique($this->whitelist); + } } /** @@ -1730,6 +1789,15 @@ class Cortex extends Cursor { return $out; } + /** + * return raw value of a field + * @param $key + * @return mixed + */ + function &getRaw($key) { + return $this->get($key, true); + } + /** * find the ID values of given relation collection * @param $val string|array|object|bool @@ -1741,7 +1809,7 @@ class Cortex extends Cursor { if (is_null($val)) return NULL; if (is_object($val) && $val instanceof CortexCollection) - $val = $val->expose(); + $val = $val->getAll($rel_field,true); elseif (is_string($val)) // split-able string of collection IDs $val = \Base::instance()->split($val); @@ -1863,9 +1931,9 @@ class Cortex extends Cursor { $rel_depths = array('*'=>$rel_depths-1); elseif (is_array($rel_depths)) $rel_depths['*'] = isset($rel_depths['*'])?--$rel_depths['*']:-1; - if (!empty($this->fieldConf)) { + if ($this->fieldConf) { $fields += array_fill_keys(array_keys($this->fieldConf),NULL); - if($this->whitelist) + if ($this->whitelist) $fields = array_intersect_key($fields, array_flip($this->whitelist)); $mp = $obj ? : $this; foreach ($fields as $key => &$val) { @@ -2052,6 +2120,18 @@ class Cortex extends Cursor { ($relField && isset($this->fieldConf[$key]['relType'])); } + /** + * return TRUE if any/specified field value has changed + * @param string $key + * @return mixed + */ + public function changed($key=null) { + if (method_exists($this->mapper,'changed')) + return $this->mapper->changed($key); + else + trigger_error('method does not exist on mapper',E_USER_ERROR); + } + /** * clear any mapper field or relation * @param string $key @@ -2180,24 +2260,24 @@ class CortexQueryParser extends \Prefab { // enhanced IN handling if (is_int(strpos($part, '?'))) { $val = array_shift($args); - if (is_int($pos = strpos($part, 'IN ?'))) { + if (is_int($pos = strpos($part, ' IN ?'))) { + if ($val instanceof CortexCollection) + $val = $val->getAll('_id',TRUE); if (!is_array($val) || empty($val)) trigger_error(self::E_INBINDVALUE,E_USER_ERROR); - $bindMarks = str_repeat('?,', count($val) - 1).'?'; - $part = substr($part, 0, $pos).'IN ('.$bindMarks.')'; + $bindMarks = str_repeat('?,',count($val) - 1).'?'; + $part = substr($part, 0, $pos).' IN ('.$bindMarks.') '; $ncond = array_merge($ncond, $val); - } elseif($val === null && preg_match('/(\w+)\s*([!=<>]+)\s*\?/i',$part,$match)) { - $part = $match[1].' IS '.($match[2]=='='||$match[2]=='=='?'':'NOT ').'NULL'; + } elseif($val === null && + preg_match('/((?:\S[\w\-]+\S.?)+)\s*'. + '(!?==?)\s*(?:\?|:\w+)/i',$part,$match)) { + $part = ' '.$match[1].' IS '.($match[2][0]=='!'?'NOT ':'').'NULL '; } else $ncond[] = $val; } unset($part); } array_unshift($ncond, implode($parts)); -// array_unshift($ncond, array_reduce($parts,function($out,$part){ -// return $out.((!$out||in_array($part,array('(',')')) -// ||preg_match('/\($/',$out))?'':' ').$part; -// },'')); break; default: trigger_error(self::E_ENGINEERROR,E_USER_ERROR); @@ -2233,7 +2313,7 @@ class CortexQueryParser extends \Prefab { $pos = 0; foreach ($parts as &$part) { if (preg_match('/:\w+/i', $part, $match)) { - if (!isset($args[$match[0]])) + if (!array_key_exists($match[0],$args)) trigger_error(sprintf(self::E_MISSINGBINDKEY, $match[0]),E_USER_ERROR); $part = str_replace($match[0], '?', $part); @@ -2249,10 +2329,9 @@ class CortexQueryParser extends \Prefab { * quote identifiers in condition * @param string $cond * @param object $db - * @param string|bool $table * @return string */ - public function sql_quoteCondition($cond, $db, $table=false) { + public function sql_quoteCondition($cond, $db) { // https://www.debuggex.com/r/6AXwJ1Y3Aac8aocQ/3 // https://regex101.com/r/yM5vK4/1 // this took me lots of sleepless nights @@ -2261,12 +2340,33 @@ class CortexQueryParser extends \Prefab { '(?:(\b(?=!)]|$))/i', // only when part of condition or within brackets - function($match) use($table,$db) { + function($match) use($db) { if (!isset($match[2])) return $match[1]; if (preg_match('/\b(AND|OR|IN|LIKE|NOT)\b/i',$match[2])) return $match[2]; - return $db->quotekey(($table?$table.'.':'').$match[2]); + return $db->quotekey($match[2]); + }, $cond); + } + + /** + * add table prefix to identifiers which do not have a table prefix yet + * @param string $cond + * @param string $table + * @return string + */ + public function sql_prependTableToFields($cond, $table) { + return preg_replace_callback('/'. + '(\w+\((?:[^)(]+|(?R))*\))|'. + '(?:(\s)|^|(?<=[(]))'. + '([a-zA-Z_](?:[\w\-_]+))'. + '(?=[\s<>=!)]|$)/i', + function($match) use($table) { + if (!isset($match[3])) + return $match[1]; + if (preg_match('/\b(AND|OR|IN|LIKE|NOT)\b/i',$match[3])) + return $match[0]; + return $match[2].$table.'.'.$match[3]; }, $cond); } @@ -2299,6 +2399,8 @@ class CortexQueryParser extends \Prefab { $part = ($not ? '!' : '').'preg_match(?,'.$match[0].')'; } // find IN operator elseif (is_int($pos = strpos($upart, ' @IN '))) { + if ($val instanceof CortexCollection) + $val = $val->getAll('_id',TRUE); if ($not = is_int($npos = strpos($upart, '@NOT'))) $pos = $npos; $part = ($not ? '!' : '').'in_array('.substr($part, 0, $pos). @@ -2420,6 +2522,8 @@ class CortexQueryParser extends \Prefab { $var = array('$not' => $var); } // find IN operator elseif (in_array($upart, array('IN','NOT IN'))) { + if ($var instanceof CortexCollection) + $var = $var->getAll('_id',true); $var = array(($upart=='NOT IN')?'$nin':'$in' => array_values($var)); } // translate operators elseif (!in_array($match[0], array('==', '='))) { @@ -2686,6 +2790,44 @@ class CortexCollection extends \ArrayIterator { unset($this[$ii]); } + /** + * compare collection with a given ID stack + * @param array|CortexCollection $stack + * @param string $cpm_key + * @return array + */ + public function compare($stack,$cpm_key='_id') { + if ($stack instanceof CortexCollection) + $stack = $stack->getAll($cpm_key,true); + $keys = $this->getAll($cpm_key,true); + $out = []; + $new = array_diff($stack,$keys); + $old = array_diff($keys,$stack); + if ($new) + $out['new'] = $new; + if ($old) + $out['old'] = $old; + return $out; + } + + /** + * check if the collection contains a record with the given key-val set + * @param mixed $val + * @param string $key + * @return bool + */ + public function contains($val,$key='_id') { + $rel_ids = $this->getAll($key, true); + if ($val instanceof Cursor) + $val = $val->{$key}; + return in_array($val,$rel_ids); + } + + /** + * create a new hydrated collection from the given records + * @param $records + * @return CortexCollection + */ static public function factory($records) { $cc = new self(); $cc->setModels($records); diff --git a/app/lib/db/sql/schema.php b/app/lib/db/sql/schema.php index 60f52d4f..bea09c30 100644 --- a/app/lib/db/sql/schema.php +++ b/app/lib/db/sql/schema.php @@ -13,12 +13,13 @@ * | | < | <| -__|-- __| * |__|__|__||__|__|_____|_____| * - * Copyright (c) 2015 by ikkez + * Copyright (c) 2016 by ikkez * Christian Knuth * https://github.com/ikkez/F3-Sugar/ * * @package DB - * @version 2.2.0-dev + * @version 2.2.1 + * @date 25.04.2017 **/ @@ -58,9 +59,10 @@ class Schema extends DB_Utils { 'mssql|sybase|dblib|odbc|sqlsrv' => 'float', 'imb' => 'decfloat' ), - 'DOUBLE' => array('mysql|sqlite2?|ibm' => 'DOUBLE', - 'pgsql' => 'double precision', - 'mssql|dblib|sybase|odbc|sqlsrv' => 'decimal', + 'DOUBLE' => array('mysql|ibm' => 'decimal(18,6)', + 'sqlite2?' => 'decimal(15,6)', // max 15-digit on sqlite + 'pgsql' => 'numeric(18,6)', + 'mssql|dblib|sybase|odbc|sqlsrv' => 'decimal(18,6)', ), 'VARCHAR128' => array('mysql|sqlite2?|ibm|mssql|sybase|dblib|odbc|sqlsrv' => 'varchar(128)', 'pgsql' => 'character varying(128)', @@ -281,7 +283,7 @@ class Schema extends DB_Utils { 'TRUNCATE TABLE '.$this->db->quotekey($name).';', 'sqlite2?' => array( 'DELETE FROM '.$this->db->quotekey($name).';', - 'UPDATE SQLITE_SEQUENCE SET seq = 0 WHERE name = '.$this->db->quotekey($name).';', +// 'UPDATE SQLITE_SEQUENCE SET seq = 0 WHERE name = '.$this->db->quotekey($name).';', ), ); $query = $this->findQuery($cmd); @@ -348,7 +350,7 @@ abstract class TableBuilder extends DB_Utils { $key = $key->name; } if (array_key_exists($key,$this->columns)) - trigger_error(sprintf(self::TEXT_ColumnExists,$key)); + trigger_error(sprintf(self::TEXT_ColumnExists,$key),E_USER_ERROR); $column = new Column($key, $this); if ($args) foreach ($args as $arg => $val) @@ -450,6 +452,12 @@ class TableCreator extends TableBuilder { const TEXT_TableAlreadyExists = "Table `%s` already exists. Cannot create it."; + protected $charset='utf8'; + + public function setCharset($str) { + $this->charset=$str; + } + /** * generate SQL query for creating a basic table, containing an ID serial field * and execute it if $exec is true, otherwise just return the generated query string @@ -460,7 +468,7 @@ class TableCreator extends TableBuilder { { // check if already existing if ($exec && in_array($this->name, $this->schema->getTables())) { - trigger_error(sprintf(self::TEXT_TableAlreadyExists,$this->name)); + trigger_error(sprintf(self::TEXT_TableAlreadyExists,$this->name),E_USER_ERROR); return false; } $cols = ''; @@ -468,7 +476,7 @@ class TableCreator extends TableBuilder { foreach ($this->columns as $cname => $column) { // no defaults for TEXT type if ($column->default !== false && is_int(strpos(strtoupper($column->type),'TEXT'))) { - trigger_error(sprintf(self::TEXT_NoDefaultForTEXT, $column->name)); + trigger_error(sprintf(self::TEXT_NoDefaultForTEXT, $column->name),E_USER_ERROR); return false; } $cols .= ', '.$column->getColumnQuery(); @@ -479,7 +487,7 @@ class TableCreator extends TableBuilder { 'sqlite2?|sybase|dblib' => "CREATE TABLE $table ($id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT".$cols.");", 'mysql' => - "CREATE TABLE $table ($id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT".$cols.") DEFAULT CHARSET=utf8 COLLATE utf8_unicode_ci;", + "CREATE TABLE $table ($id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT".$cols.") DEFAULT CHARSET=$this->charset COLLATE ".$this->charset."_unicode_ci;", 'pgsql' => "CREATE TABLE $table ($id SERIAL PRIMARY KEY".$cols.");", 'mssql|odbc|sqlsrv' => @@ -544,10 +552,9 @@ class TableModifier extends TableBuilder { */ public function build($exec = TRUE) { - // check if table exists if (!in_array($this->name, $this->schema->getTables())) - trigger_error(sprintf(self::TEXT_TableNotExisting, $this->name)); + trigger_error(sprintf(self::TEXT_TableNotExisting, $this->name),E_USER_ERROR); if ($sqlite = preg_match('/sqlite2?/', $this->db->driver())) { $sqlite_queries = array(); @@ -560,12 +567,12 @@ class TableModifier extends TableBuilder { /** @var Column $column */ // not nullable fields should have a default value, when altering a table if ($column->default === false && $column->nullable === false) { - trigger_error(sprintf(self::TEXT_NotNullFieldNeedsDefault, $column->name)); + trigger_error(sprintf(self::TEXT_NotNullFieldNeedsDefault, $column->name),E_USER_ERROR); return false; } // no defaults for TEXT type if($column->default !== false && is_int(strpos(strtoupper($column->type),'TEXT'))) { - trigger_error(sprintf(self::TEXT_NoDefaultForTEXT, $column->name)); + trigger_error(sprintf(self::TEXT_NoDefaultForTEXT, $column->name),E_USER_ERROR); return false; } $table = $this->db->quotekey($this->name); @@ -831,9 +838,9 @@ class TableModifier extends TableBuilder { $existing_columns = $this->getCols(true); // check if column is already existing if (!in_array($name, array_keys($existing_columns))) - trigger_error('cannot rename column. it does not exist.'); + trigger_error('cannot rename column. it does not exist.',E_USER_ERROR); if (in_array($new_name, array_keys($existing_columns))) - trigger_error('cannot rename column. new column already exist.'); + trigger_error('cannot rename column. new column already exist.',E_USER_ERROR); if (preg_match('/sqlite2?/', $this->db->driver())) // SQlite does not support drop or rename column directly @@ -992,7 +999,7 @@ class TableModifier extends TableBuilder { foreach($result as $row) $indexes[$row['Key_name']] = array('unique' => !(bool)$row['Non_unique']); } else - trigger_error(sprintf(self::TEXT_ENGINE_NOT_SUPPORTED, $this->db->driver())); + trigger_error(sprintf(self::TEXT_ENGINE_NOT_SUPPORTED, $this->db->driver()),E_USER_ERROR); return $indexes; } @@ -1193,7 +1200,7 @@ class Column extends DB_Utils { */ public function getTypeVal() { if (!$this->type) - trigger_error(sprintf('Cannot build a column query for `%s`: no column type set',$this->name)); + trigger_error(sprintf('Cannot build a column query for `%s`: no column type set',$this->name),E_USER_ERROR); if ($this->passThrough) $this->type_val = $this->type; else { @@ -1201,7 +1208,7 @@ class Column extends DB_Utils { if (!$this->type_val) { if (Schema::$strict) { trigger_error(sprintf(self::TEXT_NoDataType, strtoupper($this->type), - $this->db->driver())); + $this->db->driver()),E_USER_ERROR); return FALSE; } else { // auto pass-through if not found @@ -1265,7 +1272,7 @@ class Column extends DB_Utils { if ($this->type != 'TIMESTAMP' && ($this->passThrough && strtoupper($this->type) != strtoupper($stamp_type)) ) - trigger_error(self::TEXT_CurrentStampDataType); + trigger_error(self::TEXT_CurrentStampDataType,E_USER_ERROR); return $this->findQuery($this->schema->defaultTypes[strtoupper($this->default)]); } else { // static defaults @@ -1301,7 +1308,7 @@ class DB_Utils { foreach ($cmd as $backend => $val) if (preg_match('/'.$backend.'/', $this->db->driver())) return $val; - trigger_error(sprintf(self::TEXT_ENGINE_NOT_SUPPORTED, $this->db->driver())); + trigger_error(sprintf(self::TEXT_ENGINE_NOT_SUPPORTED, $this->db->driver()),E_USER_ERROR); } public function __construct(SQL $db) { diff --git a/app/main/model/charactermodel.php b/app/main/model/charactermodel.php index f0c8b345..71c98788 100644 --- a/app/main/model/charactermodel.php +++ b/app/main/model/charactermodel.php @@ -653,7 +653,6 @@ class CharacterModel extends BasicModel { if( !$characterLog = $this->getLog() ){ // create new log $characterLog = $this->rel('characterLog'); - $characterLog->characterId = $this->_id; } // get current log data and modify on change @@ -720,6 +719,7 @@ class CharacterModel extends BasicModel { } $characterLog->setData($logData); + $characterLog->characterId = $this; $characterLog->save(); $this->characterLog = $characterLog; @@ -745,6 +745,7 @@ class CharacterModel extends BasicModel { $deleteLog = true; } + //in case of failure (invalid API response) increase or reset "retry counter" if( $user = $this->getUser() ){ // Session data does not exists in CLI mode (Cronjob) if( $sessionCharacterData = $user->getSessionCharacterData($this->id, false) ){ @@ -840,7 +841,7 @@ class CharacterModel extends BasicModel { $this->hasLog() && !$this->characterLog->dry() ){ - $characterLog = &$this->characterLog; + $characterLog = $this->characterLog; } return $characterLog; diff --git a/app/main/model/mapmodel.php b/app/main/model/mapmodel.php index bad51811..bc517d09 100644 --- a/app/main/model/mapmodel.php +++ b/app/main/model/mapmodel.php @@ -573,11 +573,9 @@ class MapModel extends BasicModel { /** * get all (private) characters for this map - * @param array $characterIds - * @param array $options * @return CharacterModel[] */ - private function getCharacters($characterIds = [], $options = []){ + private function getCharacters(){ $characters = []; $filter = ['active = ?', 1]; @@ -588,11 +586,6 @@ class MapModel extends BasicModel { $this->filter('mapCharacters', $filter); - if($options['hasLog']){ - // just characters with active log data - $this->has('mapCharacters.characterLog', ['active = ?', 1]); - } - if($this->mapCharacters){ foreach($this->mapCharacters as $characterMapModel){ $characters[] = $characterMapModel->characterId; @@ -611,7 +604,7 @@ class MapModel extends BasicModel { $characters = []; if($this->isPrivate()){ - $activeCharacters = $this->getCharacters([], $options); + $activeCharacters = $this->getCharacters(); // add active character for each user foreach($activeCharacters as $activeCharacter){ diff --git a/js/app/map/map.js b/js/app/map/map.js index 91bce281..0e455843 100644 --- a/js/app/map/map.js +++ b/js/app/map/map.js @@ -190,7 +190,6 @@ define([ // ================================================================= // user count changed -> change tooltip content - let tooltipOptions = {placement: 'top', trigger: 'manual'}; // set tooltip color let highlight = false; @@ -203,10 +202,12 @@ define([ tooltipIconClass = 'fa-caret-down'; } - tooltipOptions.id = systemId; - tooltipOptions.highlight = highlight; - tooltipOptions.title = ''; - tooltipOptions.title += ' ' + userCounter; + let tooltipOptions = { + trigger: 'manual', + id: systemId, + highlight: highlight, + title: ' ' + userCounter + }; // show system head systemHeadExpand.velocity('stop', true).velocity({ @@ -364,6 +365,7 @@ define([ '
' + ''; + options.placement = getSystemTooltipPlacement(system); options.html = true; options.animation = true; options.template = template; @@ -405,6 +407,9 @@ define([ } } + // update tooltip placement based on system position + system.data('bs.tooltip').options.placement = getSystemTooltipPlacement(system); + // show() can be forced if(options.show === true){ system.tooltip('show'); @@ -412,11 +417,21 @@ define([ } } - - }); }; + /** + * get tooltip position based on current system position + * @param system + * @returns {string} + */ + let getSystemTooltipPlacement = (system) => { + let offsetParent = system.parent().offset(); + let offsetSystem = system.offset(); + + return (offsetSystem.top - offsetParent.top < 27) ? 'bottom' : 'top'; + }; + /** * set or change the status of a system * @param status diff --git a/public/js/v1.2.3/app/map/map.js b/public/js/v1.2.3/app/map/map.js index 91bce281..0e455843 100644 --- a/public/js/v1.2.3/app/map/map.js +++ b/public/js/v1.2.3/app/map/map.js @@ -190,7 +190,6 @@ define([ // ================================================================= // user count changed -> change tooltip content - let tooltipOptions = {placement: 'top', trigger: 'manual'}; // set tooltip color let highlight = false; @@ -203,10 +202,12 @@ define([ tooltipIconClass = 'fa-caret-down'; } - tooltipOptions.id = systemId; - tooltipOptions.highlight = highlight; - tooltipOptions.title = ''; - tooltipOptions.title += ' ' + userCounter; + let tooltipOptions = { + trigger: 'manual', + id: systemId, + highlight: highlight, + title: ' ' + userCounter + }; // show system head systemHeadExpand.velocity('stop', true).velocity({ @@ -364,6 +365,7 @@ define([ '
' + ''; + options.placement = getSystemTooltipPlacement(system); options.html = true; options.animation = true; options.template = template; @@ -405,6 +407,9 @@ define([ } } + // update tooltip placement based on system position + system.data('bs.tooltip').options.placement = getSystemTooltipPlacement(system); + // show() can be forced if(options.show === true){ system.tooltip('show'); @@ -412,11 +417,21 @@ define([ } } - - }); }; + /** + * get tooltip position based on current system position + * @param system + * @returns {string} + */ + let getSystemTooltipPlacement = (system) => { + let offsetParent = system.parent().offset(); + let offsetSystem = system.offset(); + + return (offsetSystem.top - offsetParent.top < 27) ? 'bottom' : 'top'; + }; + /** * set or change the status of a system * @param status