Explorar el Código

fixed code style in acl query builder and features

Piotr Labudda hace 8 años
padre
commit
33b2a0af53
Se han modificado 2 ficheros con 880 adiciones y 880 borrados
  1. 406 406
      SE/se-lib/AclQueryBuilder.php
  2. 474 474
      SE/se-lib/AclQueryFeatures.php

+ 406 - 406
SE/se-lib/AclQueryBuilder.php

@@ -7,412 +7,412 @@ Lib::loadClass('ACL');
 
 
 class AclQueryBuilder {
 class AclQueryBuilder {
 
 
-  public $select;
-  public $from;
-  public $where;
-  /* where: array of [ $fieldName, $comparisonSign, $value ]
-   *   $fieldName - field name, TODO: xpath, null for groups (or, and)
-   *   $comparisonSign - @see where() to check allowed (implemented) signs or function names
-   *   $value - string or another $where if group (or, and)
-   * where examples: [ $fieldName, $comparisonSign, $value ]
-     [ 'ID', '=', '1' ] // where ID = '1'
-     [ 'LABEL', 'like', '%abc%' ] // where LABEL like '%abc%'
-     [ null, 'or',  [] $values ] // where ( $values[0] or  $values[1] or  ... ) // or where $values[0] when 1 === count($values)
-     [ null, 'and', [] $values ] // where ( $values[0] and $values[1] and ... ) // or where $values[0] when 1 === count($values)
-  */
-  public $orderBy;
-  public $groupBy;
-  public $limit;
-  public $offset;
-  public $_fromPrefix;
-  public $_joinPrefix;
-  public $_joinParams;
-  public $isInstances;
-  public $isNotInstances;
-  public $_hasSelectRemoteFields;
-  public $_hasQueryRemoteFields;
-
-  public function __construct() {
-    $this->select = [];
-    $this->from = null; // (ACL | tableName)
-    $this->where = [];
-    $this->orderBy = null;
-    $this->groupBy = null;
-    $this->limit = null;
-    $this->offset = null;
-    $this->_fromPrefix = 't'; // prefix for this->from - default 't'
-    $this->_joinPrefix = []; // prefix => from (Acl | tableName)
-    $this->_joinParams = []; // prefx => params
-    $this->isInstances = [];
-    $this->isNotInstances = [];
-
-    $this->_hasSelectRemoteFields = false;
-    $this->_hasQueryRemoteFields = false;
-  }
-
-  public function from($from, $prefix = 't') {
-    DBG::log([
-      'from instanceof Core_AclBase' => ($from instanceof Core_AclBase),
-      'from instanceof AntAclBase' => ($from instanceof AntAclBase),
-      'from instanceof TableAcl' => ($from instanceof TableAcl),
-    ], 'array', "\$from class(".get_class($from).")");
-    if ($this->from) throw new Exception("Duplicate FROM");
-    $this->from = $from;
-    $this->_fromPrefix = $prefix;
-    return $this;
-  }
-
-  public function select($propertyName) { // TODO: ogc:propertyName, *, xpath, @instances, etc...
-    if (empty($propertyName)) return $this; // SKIP empty values, null's, etc.
-    DBG::log($propertyName, 'array', "AclQueryBuilder->select");
-    if (is_array($propertyName)) {
-      $hasWildcard = in_array('*', $propertyName);
-      foreach ($propertyName as $k => $v) {
-        if ('rawSelect' === $k) $this->select['__rawSelect__'] = $v;
-        else if ('*' === $v) continue;
-        else $this->select[] = $v;
-      }
-      if ($hasWildcard) array_unshift($this->select, '*');
-      return $this;
-    }
-    if (!in_array($propertyName, $this->select)) $this->select[] = $propertyName; // TODO: split by property source
-    return $this;
-  }
-
-  public function where($where) {
-    if (null === $where) return $this;
-    if (is_string($where)) return $this->whereRaw($where);
-    list($fieldName, $comparisonSign, $value) = $where;
-    if (!in_array($comparisonSign, [ // validation
-      '=', '<', '>', '<=', '>=', '<>', '!='
-      , 'like', 'not like'
-      , 'is not null', 'is null'
-      , 'Intersects', 'GeometryType'
-      , 'or' // $value = [ comparisons for $fieldName ... or null ]
-      , 'and' // $value = [ comparisons for $fieldName ... or null ]
-    ])) {
-      throw new Exception("Not implemented comparisonSign '{$comparisonSign}'");
-    }
-    $this->where[] = [$fieldName, $comparisonSign, $value];
-    return $this;
-  }
-  public function _generateWhereMain($where) { // @returns string
-    if (is_string($where)) return $where; // whereRaw
-    list($fieldName, $comparisonSign, $value) = $where;
-    $sqlFieldName = $fieldName; // TODO: getSqlFieldName // TODO: get sql field name with table prefix from join list to replace "{$this->_fromPrefix}.{$sqlFieldName}" below
-    switch ($comparisonSign) {
-      case 'is not null': return "{$this->_fromPrefix}.{$sqlFieldName} is not null";
-      case 'is null': return "{$this->_fromPrefix}.{$sqlFieldName} is null";
-      case 'Intersects': return "Intersects(GeomFromText('{$value}'), {$this->_fromPrefix}.`{$sqlFieldName}`)=1";
-      case 'GeometryType': return "GeometryType({$this->_fromPrefix}.`{$sqlFieldName}`)='{$value}'";
-      case 'or': return $this->_generateWhereBlock($where);
-      case 'and': return $this->_generateWhereBlock($where);
-      case 'UNIX_TIMESTAMP_LESS_THAN_NOW': return "
-        COALESCE(UNIX_TIMESTAMP({$this->_fromPrefix}.`{$sqlFieldName}`), 0) < UNIX_TIMESTAMP()
-        and {$this->_fromPrefix}.`{$sqlFieldName}` != ''
-        and {$this->_fromPrefix}.`{$sqlFieldName}` != '0000-00-00 00:00:00'
-      ";
-      case 'UNIX_TIMESTAMP_NOW_3600': return "
-        COALESCE(UNIX_TIMESTAMP({$this->_fromPrefix}.`{$sqlFieldName}`), 0) < UNIX_TIMESTAMP()+3600
-        and COALESCE(UNIX_TIMESTAMP({$this->_fromPrefix}.`{$sqlFieldName}`), 0) > UNIX_TIMESTAMP()-3600
-      ";
-      case 'UNIX_TIMESTAMP_GREATER_THAN': return " COALESCE(UNIX_TIMESTAMP({$this->_fromPrefix}.`{$sqlFieldName}`), 0) > '{$value}' ";
-      case 'UNIX_TIMESTAMP_LESS_THAN':    return " COALESCE(UNIX_TIMESTAMP({$this->_fromPrefix}.`{$sqlFieldName}`), 0) < '{$value}' ";
-      default: return "{$this->_fromPrefix}.{$sqlFieldName} {$comparisonSign} " . DB::getPDO()->quote($value);
-    }
-    return null;
-  }
-  public function _generateWhereBlock($where) { // @returns string
-    list($fieldName, $sqlGlue, $list) = $where;
-    $list = array_filter($list, function ($value) { return null !== $value; });
-    $sqlList = array_filter(
-      array_map([$this, '_generateWhereMain'], $list),
-      'is_string'
-    );
-    if (1 === count($sqlList)) return $sqlList[0];
-    else return "( " . implode(" {$sqlGlue} ", $sqlList) . " )";
-  }
-  public function whereIsNotNull($fieldName) {
-    $this->where[] = "{$this->_fromPrefix}.{$fieldName} is not null";
-    return $this;
-  }
-  public function whereRaw($rawWhere) { // add where without validation
-    if (!$rawWhere) return $this;
-    $this->where[] = $rawWhere;
-    return $this;
-  }
-  // public function whereFunction($column, $operator = null, $value = null) {
-  public function whereFunction($fieldName, $comparisonSign = null, $value = null) {
-    throw new Exception("whereFunction not supported");
-    $this->where[] = '';
-    return $this;
-  }
-  public function generateWhereSql() { // @return string - sql without where keyword
-    $sqlWhere = array_filter(
-      array_map([$this, '_generateWhereMain'], $this->where),
-      'is_string'
-    );
-    return (!empty($sqlWhere)) ? implode("\n\t and ", $sqlWhere) : '';
-  }
-
-  public function _generateSelectMain($select, $key) {
-    if ('__rawSelect__' === $key) return $select;
-    $sqlPk = 'ID';
-    if ($this->from instanceof Core_AclBase) $sqlPk = $this->from->getSqlPrimaryKeyField();
-    if ('*' === $select) return "{$this->_fromPrefix}.*";
-    if ('@instances' === $select) {
-      if (!($this->from instanceof Core_AclBase)) throw new Exception("select @instances allowed only for Acl object");
-      $instanceTable = ACL::getInstanceTable($this->from->getNamespace());
-      return "
-        (
-          select GROUP_CONCAT(inst_conf.namespace)
-          from `{$instanceTable}` inst_tbl
-          join `CRM_INSTANCE_CONFIG` inst_conf on (inst_conf.id = inst_tbl.idInstance)
-          where inst_tbl.pk = {$this->_fromPrefix}.{$sqlPk}
-        ) as `@instances`
-      ";
-    }
-    if (is_array($select)) {
-      // TODO: [ '__backRef' => [ ... ] ]
-      DBG::log($select, 'array', "TODO: select array");
-      return null;
-    }
-    if (is_string($select)) {
-      // TODO: only real table field
-      // TODO: if geometry type then `AsWKT(t.`{$fieldName}`) as {$fieldName}`
-      try {
-        return $this->parseSelectFieldValueToSql($select, $this->_fromPrefix);
-      } catch (Exception $e) {
-        DBG::log($e);
-      }
-    }
-    return null;
-  }
-
-  public function join($join, $prefix, $params) {
-    if (array_key_exists($prefix, $this->_joinPrefix)) throw new Exception("Prefix already used!");
-    $this->_joinPrefix[$prefix] = $join;
-    $this->_joinParams[$prefix] = $params;
-    return $this;
-  }
-
-  public function orderBy($orderBy) { // TODO: ogc: order by ...
-    if (null !== $this->orderBy) throw new Exception("OrderBy already set!");
-    $this->orderBy = [];
-    if (!$orderBy) return $this;
-    // ID A,COL_X D,COL_Y A,...
+	public $select;
+	public $from;
+	public $where;
+	/* where: array of [ $fieldName, $comparisonSign, $value ]
+	 *	 $fieldName - field name, TODO: xpath, null for groups (or, and)
+	 *	 $comparisonSign - @see where() to check allowed (implemented) signs or function names
+	 *	 $value - string or another $where if group (or, and)
+	 * where examples: [ $fieldName, $comparisonSign, $value ]
+		 [ 'ID', '=', '1' ] // where ID = '1'
+		 [ 'LABEL', 'like', '%abc%' ] // where LABEL like '%abc%'
+		 [ null, 'or',	[] $values ] // where ( $values[0] or	$values[1] or	... ) // or where $values[0] when 1 === count($values)
+		 [ null, 'and', [] $values ] // where ( $values[0] and $values[1] and ... ) // or where $values[0] when 1 === count($values)
+	*/
+	public $orderBy;
+	public $groupBy;
+	public $limit;
+	public $offset;
+	public $_fromPrefix;
+	public $_joinPrefix;
+	public $_joinParams;
+	public $isInstances;
+	public $isNotInstances;
+	public $_hasSelectRemoteFields;
+	public $_hasQueryRemoteFields;
+
+	public function __construct() {
+		$this->select = [];
+		$this->from = null; // (ACL | tableName)
+		$this->where = [];
+		$this->orderBy = null;
+		$this->groupBy = null;
+		$this->limit = null;
+		$this->offset = null;
+		$this->_fromPrefix = 't'; // prefix for this->from - default 't'
+		$this->_joinPrefix = []; // prefix => from (Acl | tableName)
+		$this->_joinParams = []; // prefx => params
+		$this->isInstances = [];
+		$this->isNotInstances = [];
+
+		$this->_hasSelectRemoteFields = false;
+		$this->_hasQueryRemoteFields = false;
+	}
+
+	public function from($from, $prefix = 't') {
+		DBG::log([
+			'from instanceof Core_AclBase' => ($from instanceof Core_AclBase),
+			'from instanceof AntAclBase' => ($from instanceof AntAclBase),
+			'from instanceof TableAcl' => ($from instanceof TableAcl),
+		], 'array', "\$from class(".get_class($from).")");
+		if ($this->from) throw new Exception("Duplicate FROM");
+		$this->from = $from;
+		$this->_fromPrefix = $prefix;
+		return $this;
+	}
+
+	public function select($propertyName) { // TODO: ogc:propertyName, *, xpath, @instances, etc...
+		if (empty($propertyName)) return $this; // SKIP empty values, null's, etc.
+		DBG::log($propertyName, 'array', "AclQueryBuilder->select");
+		if (is_array($propertyName)) {
+			$hasWildcard = in_array('*', $propertyName);
+			foreach ($propertyName as $k => $v) {
+				if ('rawSelect' === $k) $this->select['__rawSelect__'] = $v;
+				else if ('*' === $v) continue;
+				else $this->select[] = $v;
+			}
+			if ($hasWildcard) array_unshift($this->select, '*');
+			return $this;
+		}
+		if (!in_array($propertyName, $this->select)) $this->select[] = $propertyName; // TODO: split by property source
+		return $this;
+	}
+
+	public function where($where) {
+		if (null === $where) return $this;
+		if (is_string($where)) return $this->whereRaw($where);
+		list($fieldName, $comparisonSign, $value) = $where;
+		if (!in_array($comparisonSign, [ // validation
+			'=', '<', '>', '<=', '>=', '<>', '!='
+			, 'like', 'not like'
+			, 'is not null', 'is null'
+			, 'Intersects', 'GeometryType'
+			, 'or' // $value = [ comparisons for $fieldName ... or null ]
+			, 'and' // $value = [ comparisons for $fieldName ... or null ]
+		])) {
+			throw new Exception("Not implemented comparisonSign '{$comparisonSign}'");
+		}
+		$this->where[] = [$fieldName, $comparisonSign, $value];
+		return $this;
+	}
+	public function _generateWhereMain($where) { // @returns string
+		if (is_string($where)) return $where; // whereRaw
+		list($fieldName, $comparisonSign, $value) = $where;
+		$sqlFieldName = $fieldName; // TODO: getSqlFieldName // TODO: get sql field name with table prefix from join list to replace "{$this->_fromPrefix}.{$sqlFieldName}" below
+		switch ($comparisonSign) {
+			case 'is not null': return "{$this->_fromPrefix}.{$sqlFieldName} is not null";
+			case 'is null': return "{$this->_fromPrefix}.{$sqlFieldName} is null";
+			case 'Intersects': return "Intersects(GeomFromText('{$value}'), {$this->_fromPrefix}.`{$sqlFieldName}`)=1";
+			case 'GeometryType': return "GeometryType({$this->_fromPrefix}.`{$sqlFieldName}`)='{$value}'";
+			case 'or': return $this->_generateWhereBlock($where);
+			case 'and': return $this->_generateWhereBlock($where);
+			case 'UNIX_TIMESTAMP_LESS_THAN_NOW': return "
+				COALESCE(UNIX_TIMESTAMP({$this->_fromPrefix}.`{$sqlFieldName}`), 0) < UNIX_TIMESTAMP()
+				and {$this->_fromPrefix}.`{$sqlFieldName}` != ''
+				and {$this->_fromPrefix}.`{$sqlFieldName}` != '0000-00-00 00:00:00'
+			";
+			case 'UNIX_TIMESTAMP_NOW_3600': return "
+				COALESCE(UNIX_TIMESTAMP({$this->_fromPrefix}.`{$sqlFieldName}`), 0) < UNIX_TIMESTAMP()+3600
+				and COALESCE(UNIX_TIMESTAMP({$this->_fromPrefix}.`{$sqlFieldName}`), 0) > UNIX_TIMESTAMP()-3600
+			";
+			case 'UNIX_TIMESTAMP_GREATER_THAN': return " COALESCE(UNIX_TIMESTAMP({$this->_fromPrefix}.`{$sqlFieldName}`), 0) > '{$value}' ";
+			case 'UNIX_TIMESTAMP_LESS_THAN':		return " COALESCE(UNIX_TIMESTAMP({$this->_fromPrefix}.`{$sqlFieldName}`), 0) < '{$value}' ";
+			default: return "{$this->_fromPrefix}.{$sqlFieldName} {$comparisonSign} " . DB::getPDO()->quote($value);
+		}
+		return null;
+	}
+	public function _generateWhereBlock($where) { // @returns string
+		list($fieldName, $sqlGlue, $list) = $where;
+		$list = array_filter($list, function ($value) { return null !== $value; });
+		$sqlList = array_filter(
+			array_map([$this, '_generateWhereMain'], $list),
+			'is_string'
+		);
+		if (1 === count($sqlList)) return $sqlList[0];
+		else return "( " . implode(" {$sqlGlue} ", $sqlList) . " )";
+	}
+	public function whereIsNotNull($fieldName) {
+		$this->where[] = "{$this->_fromPrefix}.{$fieldName} is not null";
+		return $this;
+	}
+	public function whereRaw($rawWhere) { // add where without validation
+		if (!$rawWhere) return $this;
+		$this->where[] = $rawWhere;
+		return $this;
+	}
+	// public function whereFunction($column, $operator = null, $value = null) {
+	public function whereFunction($fieldName, $comparisonSign = null, $value = null) {
+		throw new Exception("whereFunction not supported");
+		$this->where[] = '';
+		return $this;
+	}
+	public function generateWhereSql() { // @return string - sql without where keyword
+		$sqlWhere = array_filter(
+			array_map([$this, '_generateWhereMain'], $this->where),
+			'is_string'
+		);
+		return (!empty($sqlWhere)) ? implode("\n\t and ", $sqlWhere) : '';
+	}
+
+	public function _generateSelectMain($select, $key) {
+		if ('__rawSelect__' === $key) return $select;
+		$sqlPk = 'ID';
+		if ($this->from instanceof Core_AclBase) $sqlPk = $this->from->getSqlPrimaryKeyField();
+		if ('*' === $select) return "{$this->_fromPrefix}.*";
+		if ('@instances' === $select) {
+			if (!($this->from instanceof Core_AclBase)) throw new Exception("select @instances allowed only for Acl object");
+			$instanceTable = ACL::getInstanceTable($this->from->getNamespace());
+			return "
+				(
+					select GROUP_CONCAT(inst_conf.namespace)
+					from `{$instanceTable}` inst_tbl
+					join `CRM_INSTANCE_CONFIG` inst_conf on (inst_conf.id = inst_tbl.idInstance)
+					where inst_tbl.pk = {$this->_fromPrefix}.{$sqlPk}
+				) as `@instances`
+			";
+		}
+		if (is_array($select)) {
+			// TODO: [ '__backRef' => [ ... ] ]
+			DBG::log($select, 'array', "TODO: select array");
+			return null;
+		}
+		if (is_string($select)) {
+			// TODO: only real table field
+			// TODO: if geometry type then `AsWKT(t.`{$fieldName}`) as {$fieldName}`
+			try {
+				return $this->parseSelectFieldValueToSql($select, $this->_fromPrefix);
+			} catch (Exception $e) {
+				DBG::log($e);
+			}
+		}
+		return null;
+	}
+
+	public function join($join, $prefix, $params) {
+		if (array_key_exists($prefix, $this->_joinPrefix)) throw new Exception("Prefix already used!");
+		$this->_joinPrefix[$prefix] = $join;
+		$this->_joinParams[$prefix] = $params;
+		return $this;
+	}
+
+	public function orderBy($orderBy) { // TODO: ogc: order by ...
+		if (null !== $this->orderBy) throw new Exception("OrderBy already set!");
+		$this->orderBy = [];
+		if (!$orderBy) return $this;
+		// ID A,COL_X D,COL_Y A,...
 	if (false !== strpos($orderBy, '+')) $orderBy = str_replace('+', ' ', $orderBy);
 	if (false !== strpos($orderBy, '+')) $orderBy = str_replace('+', ' ', $orderBy);
-    $sortByEx = array_map('trim', explode(',', $orderBy));
-    $sortByEx = array_filter($sortByEx, ['V', 'filterNotEmpty']);
-    foreach ($sortByEx as $sortPart) {
-      $sortPartEx = explode(' ', $sortPart);
-      if (count($sortPartEx) > 2) throw new Exception("SortBy parse error #" . __LINE__);
-      $fieldName = trim($sortPartEx[0]);
-      if (!$this->isFieldAllowedToOrderBy($fieldName)) throw new Exception("SortBy parse error for field '{$fieldName}' #" . __LINE__);
-      $colSortDir = 'ASC';
-      if (count($sortPartEx) == 2) {
-        if ('A' == strtoupper($sortPartEx[1]) || 'ASC' == strtoupper($sortPartEx[1])) {
-        } else if ('D' == strtoupper($sortPartEx[1]) || 'DESC' == strtoupper($sortPartEx[1])) {
-          $colSortDir = 'DESC';
-        } else throw new Exception("SortBy parse error - unknown sort order '{$sortPartEx[1]}' #" . __LINE__);
-      }
-      $this->orderBy[] = [$fieldName, $colSortDir];
-      DBG::log($this->orderBy, 'array', "sortBy");
-    }
-    return $this;
-  }
-  public function isFieldAllowedToOrderBy($fieldName) {
-    return true; // TODO:? only local fields in $this->from
-  }
-  public function generateOrderBySql() {
-    if (empty($this->orderBy)) return '';
-    $sortByList = [];
-    foreach ($this->orderBy as $orderBy) {
-      $sortByList[] = "t.`{$orderBy[0]}` {$orderBy[1]}";
-    }
-    return (!empty($sortByList))
-      ? "order by " . implode(", ", $sortByList)
-      : '';
-  }
-
-  public function limit($limit) {
-    $this->limit = (int)$limit;
-    return $this;
-  }
-
-  public function offset($offset) {
-    $this->offset = (int)$offset;
-    return $this;
-  }
-
-  public function isInstance($instances) {
-    $this->isInstances = (is_array($instances)) ? $instances : [ $instances ];
-    return $this;
-  }
-
-  public function isNotInstance($instances) {
-    $this->isNotInstances = (is_array($instances)) ? $instances : [ $instances ];
-    return $this;
-  }
-
-  public function _parseJoinParams($params) {
-    if (array_key_exists('rawJoin', $params)) return $params['rawJoin'];
-    throw new Exception("Not implemented JOIN params '".json_encode($params)."'");
-  }
-
-  public function _getTableName($source) {
-    if (is_scalar($source)) return $source;
-    if ($source instanceof Core_AclBase) return $source->getRootTableName();
-    throw new Exception("Not implemented FROM type '".get_class($source)."'");
-  }
-
-  public function execute() {
-    return $this->fetchAll();
-  }
-
-  public function fetchAll() {
-    $sql = $this->generateSql();
-    DBG::log((array)$this, 'array', "AclQueryBuilder::fetchAll");
-    return DB::getPDO()->fetchAll($sql);
-  }
-
-  public function fetchTotal() {
-    $sql = $this->generateSql("count(*) as cnt");
-    return DB::getPDO()->fetchValue($sql);
-  }
-
-  public function fetchValue() {
-    $sql = $this->generateSql();
-    DBG::log(['sql'=>$sql,'this'=>(array)$this], 'array', "AclQueryBuilder::fetchValue");
-    return DB::getPDO()->fetchValue($sql);
-  }
-
-  public function fetchFirst() {
-    $sql = $this->generateSql();
-    DBG::log(['sql'=>$sql,'this'=>(array)$this], 'array', "AclQueryBuilder::fetchFirst");
-    return DB::getPDO()->fetchFirst($sql);
-  }
-
-  public function generateSql($select = null) {
-    if (!$this->from) throw new Exception("Missing FROM");
-    $tableName = $this->_getTableName($this->from);
-    if (!$tableName) throw new Exception("Missing FROM table name");
-
-    $sqlPk = ($this->from instanceof Core_AclBase)
-      ? $this->from->getSqlPrimaryKeyField()
-      : 'ID';
-
-    $sqlJoin = [];
-    DBG::log($this, 'array', '$this');
-    foreach ($this->isInstances as $k => $ns) {
-      $idInstance = ACL::getInstanceId($ns);
-      $instanceTable = ACL::getInstanceTable($ns);
-      // ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPk} and i.idInstance = {$idInstance}" ])
-      $prefix = "is_inst_{$k}";
-      // $sqlJoin[] = " inner join `{$joinName}` {$prefix} on (
-      //   {$prefix}.pk = {$this->_fromPrefix}.{$sqlPk}
-      //   and {$prefix}.idInstance = {$idInstance}
-      // )";
-      $this->where[] = "{$this->_fromPrefix}.{$sqlPk} in (
-        select {$prefix}.pk
-        from `{$instanceTable}` {$prefix}
-        where {$prefix}.idInstance = {$idInstance}
-      )";
-      DBG::log("{$this->_fromPrefix}.{$sqlPk} in (
-        select {$prefix}.pk
-        from `{$instanceTable}` {$prefix}
-        where {$prefix}.idInstance = {$idInstance}
-      )", 'string', "\$this->where[] =");
-    }
-    foreach ($this->isNotInstances as $k => $ns) {
-      $idInstance = ACL::getInstanceId($ns);
-      $instanceTable = ACL::getInstanceTable($ns);
-      // ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPk} and i.idInstance = {$idInstance}" ])
-      $prefix = "is_inst_{$k}";
-      // $sqlJoin[] = " inner join `{$joinName}` {$prefix} on (
-      //   {$prefix}.pk = {$this->_fromPrefix}.{$sqlPk}
-      //   and {$prefix}.idInstance = {$idInstance}
-      // )";
-      $this->where[] = "{$this->_fromPrefix}.{$sqlPk} not in (
-        select {$prefix}.pk
-        from `{$instanceTable}` {$prefix}
-        where {$prefix}.idInstance = {$idInstance}
-      )";
-    }
-    // join `{$instanceTable}` i on(i.pk = t.{$sqlPk} and i.idInstance = {$idInstance})
-    foreach ($this->_joinPrefix as $prefix => $join) {
-      $joinName = $this->_getTableName($join);
-      $sqlJoin[] = " join `{$joinName}` {$prefix} on(" . $this->_parseJoinParams($this->_joinParams[$prefix]) . ")";
-    }
-
-    $sqlJoin = (!empty($sqlJoin)) ? implode("\n\t", $sqlJoin) : "";
-
-    DBG::log($this->where, 'array', "generateSql \$this->where");
-    $sqlWhere = array_filter(
-      array_map([$this, '_generateWhereMain'], $this->where),
-      'is_string'
-    );
-    $sqlWhere = (!empty($sqlWhere)) ? "where " . implode("\n\t and ", $sqlWhere) : '';
-
-    $limit = ($this->limit < 0) ? 0 : $this->limit;
-    $offset = ($this->offset < 0) ? 0 : $this->offset;
-    $sqlLimit = ($limit > 0) ? "limit {$limit} offset {$offset}" : '';
-
-    // TODO: split select to local and remote (use field isLocal)
-    // TODO: $this->_hasSelectRemoteFields = false;
-    // TODO: $this->_hasQueryRemoteFields = false;
-
-    if ($select) {
-      $sqlSelect = $select; // used for example fetchTotal -> generateSql("count(*) as cnt")
-    } else {
-      $sqlSelect = array_filter(
-        array_map([$this, '_generateSelectMain'], $this->select, array_keys($this->select)),
-        'is_string'
-      );
-      $sqlSelect = (!empty($sqlSelect))
-        ? implode(",\n\t", $sqlSelect)
-        : "{$this->_fromPrefix}.*"
-      ;
-    }
-
-    $sqlOrderBy = $this->generateOrderBySql();
-    return "
-      select {$sqlSelect}
-      from `{$tableName}` {$this->_fromPrefix}
-        {$sqlJoin}
-      {$sqlWhere}
-      {$sqlOrderBy}
-      {$sqlLimit}
-    ";
-  }
-
-  public function parseSelectFieldValueToSql($fieldName, $prefix = 't') {
-    if (false !== strpos($fieldName, '/')) {
-      DBG::log($fieldName, 'string', "TODO: select by xpath");
-      throw new Exception("Not implemented select by xpath ('{$fieldName}')");
-    }
-
-    $fieldType = $this->from->getXsdFieldType($fieldName);
-    @list($typePrefix, $typeName, $retTypeName) = explode(':', $fieldType);
-    switch ($typePrefix) {
-      case 'xsd': return "{$prefix}.`{$fieldName}`";
-      // 'gml:PolygonPropertyType':
-      // 'gml:PointPropertyType':
-      // 'gml:LineStringPropertyType':
-      // 'gml:GeometryPropertyType':
-      case 'gml': return "AsWKT({$prefix}.`{$fieldName}`) as `{$fieldName}`";
-      case 'p5': {
-        switch ($typeName) {
-          case 'price': return "{$prefix}.`{$fieldName}`";
-          case 'enum': return "{$prefix}.`{$fieldName}`"; // TODO: check if local or remote
-          case 'www_link': return "{$prefix}.`{$fieldName}`"; // TODO: check if local or remote?
-          case 'string': return "{$prefix}.`{$fieldName}`"; // TODO: check if local or remote?
-          default: throw new Exception("Not implemented field type in select '{$fieldType}' (field: '{$fieldName}')");
-        }
-      }
-      default: throw new Exception("Not implemented field type in select '{$fieldType}' (field: '{$fieldName}')");
-    }
-    return null;
-  }
+		$sortByEx = array_map('trim', explode(',', $orderBy));
+		$sortByEx = array_filter($sortByEx, ['V', 'filterNotEmpty']);
+		foreach ($sortByEx as $sortPart) {
+			$sortPartEx = explode(' ', $sortPart);
+			if (count($sortPartEx) > 2) throw new Exception("SortBy parse error #" . __LINE__);
+			$fieldName = trim($sortPartEx[0]);
+			if (!$this->isFieldAllowedToOrderBy($fieldName)) throw new Exception("SortBy parse error for field '{$fieldName}' #" . __LINE__);
+			$colSortDir = 'ASC';
+			if (count($sortPartEx) == 2) {
+				if ('A' == strtoupper($sortPartEx[1]) || 'ASC' == strtoupper($sortPartEx[1])) {
+				} else if ('D' == strtoupper($sortPartEx[1]) || 'DESC' == strtoupper($sortPartEx[1])) {
+					$colSortDir = 'DESC';
+				} else throw new Exception("SortBy parse error - unknown sort order '{$sortPartEx[1]}' #" . __LINE__);
+			}
+			$this->orderBy[] = [$fieldName, $colSortDir];
+			DBG::log($this->orderBy, 'array', "sortBy");
+		}
+		return $this;
+	}
+	public function isFieldAllowedToOrderBy($fieldName) {
+		return true; // TODO:? only local fields in $this->from
+	}
+	public function generateOrderBySql() {
+		if (empty($this->orderBy)) return '';
+		$sortByList = [];
+		foreach ($this->orderBy as $orderBy) {
+			$sortByList[] = "t.`{$orderBy[0]}` {$orderBy[1]}";
+		}
+		return (!empty($sortByList))
+			? "order by " . implode(", ", $sortByList)
+			: '';
+	}
+
+	public function limit($limit) {
+		$this->limit = (int)$limit;
+		return $this;
+	}
+
+	public function offset($offset) {
+		$this->offset = (int)$offset;
+		return $this;
+	}
+
+	public function isInstance($instances) {
+		$this->isInstances = (is_array($instances)) ? $instances : [ $instances ];
+		return $this;
+	}
+
+	public function isNotInstance($instances) {
+		$this->isNotInstances = (is_array($instances)) ? $instances : [ $instances ];
+		return $this;
+	}
+
+	public function _parseJoinParams($params) {
+		if (array_key_exists('rawJoin', $params)) return $params['rawJoin'];
+		throw new Exception("Not implemented JOIN params '".json_encode($params)."'");
+	}
+
+	public function _getTableName($source) {
+		if (is_scalar($source)) return $source;
+		if ($source instanceof Core_AclBase) return $source->getRootTableName();
+		throw new Exception("Not implemented FROM type '".get_class($source)."'");
+	}
+
+	public function execute() {
+		return $this->fetchAll();
+	}
+
+	public function fetchAll() {
+		$sql = $this->generateSql();
+		DBG::log((array)$this, 'array', "AclQueryBuilder::fetchAll");
+		return DB::getPDO()->fetchAll($sql);
+	}
+
+	public function fetchTotal() {
+		$sql = $this->generateSql("count(*) as cnt");
+		return DB::getPDO()->fetchValue($sql);
+	}
+
+	public function fetchValue() {
+		$sql = $this->generateSql();
+		DBG::log(['sql'=>$sql,'this'=>(array)$this], 'array', "AclQueryBuilder::fetchValue");
+		return DB::getPDO()->fetchValue($sql);
+	}
+
+	public function fetchFirst() {
+		$sql = $this->generateSql();
+		DBG::log(['sql'=>$sql,'this'=>(array)$this], 'array', "AclQueryBuilder::fetchFirst");
+		return DB::getPDO()->fetchFirst($sql);
+	}
+
+	public function generateSql($select = null) {
+		if (!$this->from) throw new Exception("Missing FROM");
+		$tableName = $this->_getTableName($this->from);
+		if (!$tableName) throw new Exception("Missing FROM table name");
+
+		$sqlPk = ($this->from instanceof Core_AclBase)
+			? $this->from->getSqlPrimaryKeyField()
+			: 'ID';
+
+		$sqlJoin = [];
+		DBG::log($this, 'array', '$this');
+		foreach ($this->isInstances as $k => $ns) {
+			$idInstance = ACL::getInstanceId($ns);
+			$instanceTable = ACL::getInstanceTable($ns);
+			// ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPk} and i.idInstance = {$idInstance}" ])
+			$prefix = "is_inst_{$k}";
+			// $sqlJoin[] = " inner join `{$joinName}` {$prefix} on (
+			//	 {$prefix}.pk = {$this->_fromPrefix}.{$sqlPk}
+			//	 and {$prefix}.idInstance = {$idInstance}
+			// )";
+			$this->where[] = "{$this->_fromPrefix}.{$sqlPk} in (
+				select {$prefix}.pk
+				from `{$instanceTable}` {$prefix}
+				where {$prefix}.idInstance = {$idInstance}
+			)";
+			DBG::log("{$this->_fromPrefix}.{$sqlPk} in (
+				select {$prefix}.pk
+				from `{$instanceTable}` {$prefix}
+				where {$prefix}.idInstance = {$idInstance}
+			)", 'string', "\$this->where[] =");
+		}
+		foreach ($this->isNotInstances as $k => $ns) {
+			$idInstance = ACL::getInstanceId($ns);
+			$instanceTable = ACL::getInstanceTable($ns);
+			// ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPk} and i.idInstance = {$idInstance}" ])
+			$prefix = "is_inst_{$k}";
+			// $sqlJoin[] = " inner join `{$joinName}` {$prefix} on (
+			//	 {$prefix}.pk = {$this->_fromPrefix}.{$sqlPk}
+			//	 and {$prefix}.idInstance = {$idInstance}
+			// )";
+			$this->where[] = "{$this->_fromPrefix}.{$sqlPk} not in (
+				select {$prefix}.pk
+				from `{$instanceTable}` {$prefix}
+				where {$prefix}.idInstance = {$idInstance}
+			)";
+		}
+		// join `{$instanceTable}` i on(i.pk = t.{$sqlPk} and i.idInstance = {$idInstance})
+		foreach ($this->_joinPrefix as $prefix => $join) {
+			$joinName = $this->_getTableName($join);
+			$sqlJoin[] = " join `{$joinName}` {$prefix} on(" . $this->_parseJoinParams($this->_joinParams[$prefix]) . ")";
+		}
+
+		$sqlJoin = (!empty($sqlJoin)) ? implode("\n\t", $sqlJoin) : "";
+
+		DBG::log($this->where, 'array', "generateSql \$this->where");
+		$sqlWhere = array_filter(
+			array_map([$this, '_generateWhereMain'], $this->where),
+			'is_string'
+		);
+		$sqlWhere = (!empty($sqlWhere)) ? "where " . implode("\n\t and ", $sqlWhere) : '';
+
+		$limit = ($this->limit < 0) ? 0 : $this->limit;
+		$offset = ($this->offset < 0) ? 0 : $this->offset;
+		$sqlLimit = ($limit > 0) ? "limit {$limit} offset {$offset}" : '';
+
+		// TODO: split select to local and remote (use field isLocal)
+		// TODO: $this->_hasSelectRemoteFields = false;
+		// TODO: $this->_hasQueryRemoteFields = false;
+
+		if ($select) {
+			$sqlSelect = $select; // used for example fetchTotal -> generateSql("count(*) as cnt")
+		} else {
+			$sqlSelect = array_filter(
+				array_map([$this, '_generateSelectMain'], $this->select, array_keys($this->select)),
+				'is_string'
+			);
+			$sqlSelect = (!empty($sqlSelect))
+				? implode(",\n\t", $sqlSelect)
+				: "{$this->_fromPrefix}.*"
+			;
+		}
+
+		$sqlOrderBy = $this->generateOrderBySql();
+		return "
+			select {$sqlSelect}
+			from `{$tableName}` {$this->_fromPrefix}
+				{$sqlJoin}
+			{$sqlWhere}
+			{$sqlOrderBy}
+			{$sqlLimit}
+		";
+	}
+
+	public function parseSelectFieldValueToSql($fieldName, $prefix = 't') {
+		if (false !== strpos($fieldName, '/')) {
+			DBG::log($fieldName, 'string', "TODO: select by xpath");
+			throw new Exception("Not implemented select by xpath ('{$fieldName}')");
+		}
+
+		$fieldType = $this->from->getXsdFieldType($fieldName);
+		@list($typePrefix, $typeName, $retTypeName) = explode(':', $fieldType);
+		switch ($typePrefix) {
+			case 'xsd': return "{$prefix}.`{$fieldName}`";
+			// 'gml:PolygonPropertyType':
+			// 'gml:PointPropertyType':
+			// 'gml:LineStringPropertyType':
+			// 'gml:GeometryPropertyType':
+			case 'gml': return "AsWKT({$prefix}.`{$fieldName}`) as `{$fieldName}`";
+			case 'p5': {
+				switch ($typeName) {
+					case 'price': return "{$prefix}.`{$fieldName}`";
+					case 'enum': return "{$prefix}.`{$fieldName}`"; // TODO: check if local or remote
+					case 'www_link': return "{$prefix}.`{$fieldName}`"; // TODO: check if local or remote?
+					case 'string': return "{$prefix}.`{$fieldName}`"; // TODO: check if local or remote?
+					default: throw new Exception("Not implemented field type in select '{$fieldType}' (field: '{$fieldName}')");
+				}
+			}
+			default: throw new Exception("Not implemented field type in select '{$fieldType}' (field: '{$fieldName}')");
+		}
+		return null;
+	}
 
 
 }
 }

+ 474 - 474
SE/se-lib/AclQueryFeatures.php

@@ -6,481 +6,481 @@ Lib::loadClass('ParseOgcFilter');
 Lib::loadClass('TableAcl');
 Lib::loadClass('TableAcl');
 
 
 // usage: (Acl class)::buildQuery($params): return new AclQueryFeatures($this, $params);
 // usage: (Acl class)::buildQuery($params): return new AclQueryFeatures($this, $params);
-//        (view): $queryFeatures = $acl->buildQuery($params);
-//        (view): $total = $queryFeatures->getTotal();
-//        (view): $items = $queryFeatures->getItems();
+//				(view): $queryFeatures = $acl->buildQuery($params);
+//				(view): $total = $queryFeatures->getTotal();
+//				(view): $items = $queryFeatures->getItems();
 // example: @see TableAcl, TableAjax
 // example: @see TableAcl, TableAjax
 
 
 // Special Filter Access - btns visible only if user don't have super access perms. If has, then will always see all rows.
 // Special Filter Access - btns visible only if user don't have super access perms. If has, then will always see all rows.
 
 
 class AclQueryFeatures {
 class AclQueryFeatures {
 
 
-  public $_params;
-  public $_acl;
-  public $_query;
-  public $_total;
-  public $_legacyMode;
-  public $_selectLocalFields; // TODO: leave here or move to AclQueryBuilder? use join for perf?
-  public $_selectRemote; // TODO: ...
-
-  public function __construct($acl, $params, $legacyMode = false) {
-    $this->_acl = $acl;
-    $this->_params = $params;
-    $this->_query = null;
-    $this->_total = null;
-    $this->_legacyMode = $legacyMode;
-    // TODO: _legacyMode = ($from instanceof simple schema or another programmed objects)
-    $this->_selectLocalFields = null;
-    $this->_selectRemote = null;
-  }
-
-  public function parseQueryValue($fieldName, $searchQuery, $fieldType = 'xsd:string') {
-    if ('!NULL' === $searchQuery) return ['is not null', null];
-    if ('IS NOT NULL' === $searchQuery) return ['is not null', null];
-    if ('NULL' === $searchQuery) return ['is null', null];
-    if ('IS NULL' === $searchQuery) return ['is null', null];
-    switch ($fieldType) {
-      case 'gml:PolygonPropertyType':
-      case 'gml:PointPropertyType':
-      case 'gml:LineStringPropertyType':
-      case 'gml:GeometryPropertyType': return $this->_parseGeomQuery($searchQuery);
-      //   $sqlFilter = $this->_sqlValueForGeomField($fldName, $v, 't');
-      // if ('_CSV_NUM' == substr($fldName, -8)) { // if ($this->isCsvNumericField($fldName)) { // TODO: xsd type - p5:csv_num
-      //   $sqlFilter = $this->_sqlValueForCsvNumericField($fldName, $v, 't');
-      //   if ($sqlFilter) $sql_where_and[] = $sqlFilter;
-      //   continue;
-      // }
-    }
-
-    switch (substr($searchQuery, 0, 1)) {
-      case '=': return ['=', substr($searchQuery, 1)];
-      case '>':
-        switch (substr($searchQuery, 1, 1)) {
-          case '=': return ['>=', substr($searchQuery, 2)];
-          default:  return ['>', substr($searchQuery, 1)];
-        }
-      case '<':
-        switch (substr($searchQuery, 1, 1)) {
-          case '=': return ['<=', substr($searchQuery, 2)];
-          case '>': return ['!=', substr($searchQuery, 2)];
-          default:  return ['<', substr($searchQuery, 1)];
-        }
-      case '!':
-        switch (substr($searchQuery, 1, 1)) {
-          case '=': return ['!=', substr($searchQuery, 2)];
-          default:  return ['not like', substr($searchQuery, 1)];
-        }
-      default: {
-        switch ($fieldType) {
-          case 'xsd:number':
-          case 'xsd:int':
-          case 'xsd:integer': {
-            if (false !== strpos($searchQuery, '%')) return ['like', $searchQuery];
-            return ['=', $searchQuery];
-          }
-          default: {
-            if (false !== strpos($searchQuery, '%')) return ['like', $searchQuery];
-            $queryWhereBuilder = new SqlQueryWhereBuilder();
-            return ['and'
-              , array_map(function ($word) use ($fieldName) {
-                  return [$fieldName, 'like', "%{$word}%"];
-                }, $queryWhereBuilder->splitQueryToWords($searchQuery)
-              )
-            ];
-          }
-        }
-        return ['=', $searchQuery];
-      }
-    }
-  }
-  public function _parseGeomQuery($searchQuery) { // _sqlValueForGeomField($fldName, $fltrValue, $tblPrefix = 't')
-    // example: BBOX:54.40993961633866,18.583889010112824,54.337945760687454,18.397121431987586
-    DBG::log($searchQuery, 'string', "\$searchQuery");
-    if ('BBOX:' == substr($searchQuery, 0, 5)) {
-      $valParts = explode(',', substr($searchQuery, 5));
-      if (4 !== count($valParts)) throw new Exception("Wrong BBOX query");
-      $valParts = array_filter($valParts, 'is_numeric');
-      if (4 !== count($valParts)) throw new Exception("Wrong BBOX query - expected 4 numeric values");
-      $bounds = "POLYGON((
-        {$valParts[3]} {$valParts[2]},
-        {$valParts[3]} {$valParts[0]},
-        {$valParts[1]} {$valParts[0]},
-        {$valParts[1]} {$valParts[2]},
-        {$valParts[3]} {$valParts[2]}
-      ))";
-      // for mysql 5.6 use ST_Contains() @see http://dev.mysql.com/doc/refman/5.6/en/spatial-relation-functions.html
-      return [ 'Intersects', $bounds ];
-    }
-    else if ('GeometryType=' == substr($fltrValue, 0, 13)) {
-      return [ 'GeometryType', substr($fltrValue, 13) ];
-    }
-    throw new Exception("Not implemented geometry query string"); // TODO:? return null;
-  }
-
-  public function _sqlValueForCsvNumericField($fldName, $fltrValue, $tblPrefix = 't') {
-    $sqlFilter = false;
-    if (is_numeric($fltrValue)) {
-      $sqlFilter = "FIND_IN_SET('{$fltrValue}', `{$fldName}`)>0";
-    } else if (false !== strpos($fltrValue, ' ')) {
-      $sqlGlue = " or ";
-      $fltrValues = $fltrValue;
-      if ('&' == substr($fltrValues, 0, 1)) {
-        $fltrValues = substr($fltrValues, 1);
-        $sqlGlue = " and ";
-      }
-      $fltrValues = explode(' ', $fltrValues);
-      $sqlNumericValues = array();
-      foreach ($fltrValues as $fltrVal) {
-        if (is_numeric($fltrVal)) {
-          $sqlNumericValues[] = "FIND_IN_SET('{$fltrVal}', `{$fldName}`)>0";
-        }
-      }
-      if (!empty($sqlNumericValues)) {
-        $sqlFilter = "(" . implode($sqlGlue, $sqlNumericValues) . ")";
-      }
-    }
-    return $sqlFilter;
-  }
-
-  public function parseSpecialFilterMsgs($type) {
-    $rootTableName = $this->_acl->getRootTableName();
-    DBG::log($rootTableName, 'string', "parse SpecialFilter Msgs({$type}), \$rootTableName");
-    $sqlHasFltrMsgs = "
-      select 1
-      from `CRM_UI_MSGS` m
-      where m.`uiTargetName`=CONCAT('{$rootTableName}.', t.`ID`)
-        and m.`uiTargetType`='default_db_table_record'
-        and m.`A_STATUS` not in('DELETED')
-      limit 1
-    ";
-    switch ($type) {
-      case 'HAS_MSGS': return " ({$sqlHasFltrMsgs})=1 ";
-      case 'NO_MSGS':  return " ({$sqlHasFltrMsgs}) is null ";
-      case 'NEW_MSGS': {
-        $sqlNewFltrMsgs = "
-          select 1
-          from `CRM_UI_MSGS` m
-          where m.`uiTargetName`=CONCAT('{$rootTableName}.', t.`ID`)
-            and m.`uiTargetType`='default_db_table_record'
-            and m.`A_STATUS` in('WAITING')
-          limit 1
-        ";
-        return " ({$sqlNewFltrMsgs})=1 ";
-      }
-    }
-    return null;
-  }
-
-  public function parseSpecialFilterProblemy($type) {
-    DBG::log($type, 'string', "parse SpecialFilter Problemy");
-    switch ($type) {
-      case 'PROBLEM': return ['A_PROBLEM', '!=', ''];
-      case 'WARNING': return ['A_PROBLEM', '=', 'WARNING'];
-      case 'NORMAL':  return ['A_PROBLEM', '=', 'NORMAL'];
-    }
-    return null;
-  }
-
-  public function parseSpecialFilterStatus($type) {
-    DBG::log($type, 'string', "parse SpecialFilter Status");
-    switch ($type) {
-      case 'WAITING': return ['A_STATUS', '=', 'WAITING'];
-      case 'AKTYWNI': return ['A_STATUS', 'or', [ // `A_STATUS` in('NORMAL', 'WARNING') ";
-        ['A_STATUS', '=', 'NORMAL'],
-        ['A_STATUS', '=', 'WARNING'],
-      ] ];
-    }
-    return null;
-  }
-
-  public function parseSpecialFilterSpotkania($type) {
-    DBG::log($type, 'string', "parse SpecialFilter Spotkania");
-    switch ($type) {
-      case 'OLD': return ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN_NOW'];
-      //           COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < UNIX_TIMESTAMP()
-      //           and t.`L_APPOITMENT_DATE` != ''
-      //           and t.`L_APPOITMENT_DATE` != '0000-00-00 00:00:00'
-      case 'NOW': return ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_NOW_3600'];
-      //           COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < UNIX_TIMESTAMP()+3600
-      //           and COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) > UNIX_TIMESTAMP()-3600
-      case 'TODAY': return ['L_APPOITMENT_DATE', 'and', [
-        ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d"),     date("Y"))],
-        ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN',    mktime(0,0,0, date("m"), date("d") + 1, date("Y"))],
-      ] ];
-      //         $start = mktime(0,0,0, date("m"), date("d"), date("Y"));
-      //         $end = mktime(0,0,0, date("m"), date("d") + 1, date("Y"));
-      //         $sqlFltr = "
-      //           COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) > '{$start}'
-      //           and COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < '{$end}'
-      //         ";
-      case 'TOMORROW': return ['L_APPOITMENT_DATE', 'and', [
-        ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d") + 1, date("Y"))],
-        ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN',    mktime(0,0,0, date("m"), date("d") + 2, date("Y"))],
-      ] ];
-      case 'YESTERDAY': return ['L_APPOITMENT_DATE', 'and', [
-        ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d") - 2, date("Y"))],
-        ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN',    mktime(0,0,0, date("m"), date("d") - 1, date("Y"))],
-      ] ];
-      case 'BRAK': return ['L_APPOITMENT_DATE', 'or', [
-        ['L_APPOITMENT_DATE', '=', ''],
-        ['L_APPOITMENT_DATE', '=', '0000-00-00 00:00:00'],
-      ] ];
-    }
-    return null;
-  }
-
-  public function parseSpecialFilterAccess() {
-    $userLogin = User::getLogin();
-    $usrAclGroups = User::getLdapGroupsNames();
-    DBG::log(['login'=>$userLogin, 'groups'=>$usrAclGroups, 'hasFieldWrite'=>$this->_acl->hasField('A_ADM_COMPANY'), 'hasFieldRead'=>$this->_acl->hasField('A_CLASSIFIED'), 'acl'=>$this->_acl], 'array', "parse SpecialFilter Access");
-    $orWhere = [];
-    if ($this->_acl->hasField('A_ADM_COMPANY')) {
-      $orWhere[] = ['A_ADM_COMPANY', '=', ''];// TODO: allow empty for everyone?
-      foreach ($usrAclGroups as $group) $orWhere[] = ['A_ADM_COMPANY', '=', $group];
-    }
-    if ($this->_acl->hasField('A_CLASSIFIED')) {
-      $orWhere[] = ['A_CLASSIFIED', '=', ''];// TODO: allow empty for everyone?
-      foreach ($usrAclGroups as $group) $orWhere[] = ['A_CLASSIFIED', '=', $group];
-    }
-    if (!empty($orWhere) && $this->_acl->hasField('L_APPOITMENT_USER')) {
-      $orWhere[] = ['L_APPOITMENT_USER', '=', $userLogin];
-    }
-    return (!empty($orWhere)) ? [null, 'or', $orWhere] : null;
-  }
-
-  public function parseOgcFilter($ogcFilter) {
-    $parser = new ParseOgcFilter();
-    $parser->loadOgcFilter($ogcFilter);
-    $queryWhereBuilder = $parser->convertToSqlQueryWhereBuilder();
-    return $queryWhereBuilder->getQueryWhere('t'); // TODO: $this->_fromPrefix
-  }
-
-  public function getQuery() {
-    if ($this->_query) return clone($this->_query);
-    // $ds = $this->_acl->getDataSource(); // TODO: only for TableAcl // TODO: move _parseSqlWhere to this class
-    $filtrIsInstance = []; // $filtrIsInstance = [ $this->_acl->getNamespace() ];
-    $filtrIsNotInstance = [];
-    if (!empty($this->_params['f_is_instance'])) $filtrIsInstance = $this->_params['f_is_instance'];
-    if (!empty($this->_params['f_is_not_instance'])) $filtrIsNotInstance = $this->_params['f_is_not_instance'];
-
-    $this->_query = ACL::query($this->_acl)
-      ->isInstance($filtrIsInstance)
-      ->isNotInstance($filtrIsNotInstance);
-    // ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPrimaryKey} and i.idInstance = {$idInstance}" ])
-    // $this->_query->where($ds->_parseSqlWhere($params))
-    DBG::log($this->_params, 'array', "AclQueryFeatures::getQuery \$this->_params");
-    foreach ($this->_params as $k => $v) {
-      // DBG::log(['v'=>$v, 'is_numeric' => is_numeric($k), 'is_int' => is_int($k), 'is_array' => is_array($v)], 'array', "AclQueryFeatures::getQuery \$this->_params[{$k}]");
-      if (is_int($k) && is_array($v)) {
-        $this->_query->where($v); // TODO: check format [$fieldName, $comparisonSign, $value]
-      } else if (is_int($k) && null === $v) { // skip NULL
-      } else if ('f_is_instance' === $k) { // parsed before
-      } else if ('f_is_not_instance' === $k) { // parsed before
-      } else if ('@instances' === $k) { // skip - select
-      } else if ('cols' === $k) { // skip - select
-      } else if ('f_' === substr($k, 0, 2) && is_string($v) && strlen($k) > 3) {
-        $fieldName = substr($k, 2);
-        $fieldType = $this->_acl->getXsdFieldType($fieldName);
-        list($comparisonSign, $value) = $this->parseQueryValue($fieldName, $v, $fieldType);
-        DBG::log([ $fieldName, $comparisonSign, $value, $fieldType ], 'array', "parseQueryValue");
-        $this->_query->where([$fieldName, $comparisonSign, $value]);
-      } else if ('sf_' === substr($k, 0, 3) && is_string($v) && strlen($k) > 4) {
-        switch (substr($k, 3)) {
-          case 'Msgs': $this->_query->where($this->parseSpecialFilterMsgs($v)); break;
-          case 'Problemy': $this->_query->where($this->parseSpecialFilterProblemy($v)); break;
-          case 'Status': $this->_query->where($this->parseSpecialFilterStatus($v)); break;
-          case 'Spotkania': $this->_query->where($this->parseSpecialFilterSpotkania($v)); break;
-          case 'Access': break; // SKIP - used below
-          default: throw new Exception("Not Implemented special filter '".substr($k, 3)."'");
-        }
-      } else if ('ogc:Filter' === $k) {
-        $this->_query->where($this->parseOgcFilter($v));
-      } else if ('primaryKey' === $k) {
-        $fieldName = $this->_acl->getPrimaryKeyField();
-        $fieldType = $this->_acl->getXsdFieldType($fieldName);
-        list($comparisonSign, $value) = $this->parseQueryValue($fieldName, $v, $fieldType);
-        DBG::log([ $fieldName, $comparisonSign, $value, $fieldType ], 'array', "parseQueryValue");
-        $this->_query->where([$fieldName, $comparisonSign, $value]);
-      } else if ('__backRef' === $k) { // skip - parse below
-      } else if ('limit' === $k) {
-      } else if ('limitstart' === $k) {
-      } else if ('order_by' === $k) {
-      } else if ('order_dir' === $k) {
-      } else if ('sortBy' === $k) {
-      } else {
-        throw new Exception("Not Implemented param '{$k}' = '{$v}'");
-      }
-    }
-
-    // sf_Access: if 'SHOW' then show all rows, but data with ***
-    if ('SHOW' !== V::get('sf_Access', '', $this->_params)) $this->_query->where($this->parseSpecialFilterAccess());
-
-    if (array_key_exists('__backRef', $this->_params)) {
-      $backRef = $this->_params['__backRef'];
-      if (!is_array($backRef)) throw new Exception("Wrong back ref structure - expected array");
-      if (empty($backRef['namespace'])) throw new Exception("Wrong back ref structure - missing namespace");
-      if (empty($backRef['primaryKey'])) throw new Exception("Wrong back ref structure - missing primaryKey");
-      if (empty($backRef['fieldName'])) throw new Exception("Wrong back ref structure - missing fieldName");
-      // TODO: $this->_query->where([ '__backRef' ]); or  $this->_query->join([ '__backRef' ]);
-      $refAcl = ACL::getAclByNamespace($backRef['namespace']);
-      if ($refAcl->getSourceName() !== $this->_acl->getSourceName()) throw new Exception("Not implemented join with different source");
-      $refTable = ACL::getRefTable($refAcl->getNamespace(), $backRef['fieldName']);
-      // TODO: 'in' operator? // $this->_query->where($pkField, 'in', "");
-      $sqlPk = $this->getAclSqlPrimaryKeyField();
-      $sqlBackRefPk = DB::getPDO()->quote($backRef['primaryKey']);
-      $this->_query->where("
-        t.{$sqlPk} in (
-          select refTable.REMOTE_PRIMARY_KEY
-          from `{$refTable}` refTable
-          where refTable.PRIMARY_KEY = {$sqlBackRefPk}
-        )
-      ");
-    }
-
-    return clone($this->_query);
-  }
-
-  public function getTotal() {
-    if ($this->_legacyMode) return $this->_acl->getTotal($this->_params);
-    if (null !== $this->_total) return $this->_total;
-    $this->_total = $this->getQuery()->fetchTotal();
-    return $this->_total;
-  }
-
-  public function hasParam($key) { return !empty($this->_params[$key]); }
-  public function getParam($key) { return V::get($key, '', $this->_params); }
-
-  public function getItems() {
-    if ($this->_legacyMode) return $this->_acl->getItems($this->_params);// TODO: array_map( $r => (array)$r )
-    // 'limit' => 10,
-    //  'limitstart' => 0,
-    //  'order_by' => 'ID',
-    //  'order_dir' => 'desc',
-    // TODO: sortBy from wfs query
-    $sortBy = ($this->hasParam('sortBy')) ? $this->getParam('sortBy') : null;
-    if (!$sortBy) {
-      $sortBy = $this->hasParam('order_by')
-        ? ( $this->hasParam('order_dir')
-            ? $this->getParam('order_by') . " " . $this->getParam('order_dir')
-            : $this->getParam('order_by')
-          )
-        : '';
-    }
-    $limit = V::get('limit', 10, $this->_params, 'int');
-    $offset = V::get('limitstart', 0, $this->_params, 'int');
-
-    DBG::log(['params' => $this->_params, 'sortBy' => $sortBy, 'limit' => $limit, 'offset' => $offset], 'array', '$this->_params');
-
-    $select = $this->prepareSelect();
-    DBG::log($select, 'array', "\$select is(TableAcl)=(".($this->_acl instanceof TableAcl).")");
-    DBG::log($this->getQuery(), 'array', "\$select is(TableAcl)=(".($this->_acl instanceof TableAcl).") \$this->getQuery()");
-
-    return $this->fetchRowsRefs(
-      $this->getQuery()
-        ->select($select)
-        ->limit($limit)
-        ->offset($offset)
-        ->orderBy($sortBy)
-        ->fetchAll()
-    );
-  }
-
-  public function getItem($primaryKey) { // TODO: throw exception if not found?
-    if ($this->_legacyMode) return (array)$this->_acl->getItem($primaryKey, $this->_params);
-    $select = $this->prepareSelect();
-    $pkField = $this->_acl->getPrimaryKeyField();
-    return $this->fetchRowRefs(
-      $this->getQuery()
-        ->select($select)
-        ->where([$pkField, '=', $primaryKey])
-        ->fetchFirst()
-    );
-  }
-
-  public function prepareSelect() { // TODO: replace with getSelectLocal
-    // TODO: select from params: 'cols' => [ fieldName, ... ]
-    // TODO: select from params: '@instances' => 1
-    // TODO: if no fields set, then '*'
-    // TODO: select must contain primaryKey
-    return $this->getSelectLocal();
-  }
-  public function getSelectLocal() { // @returns [ $fieldName, ... ]
-    // TODO: rawSelect!
-    if (null !== $this->_selectLocalFields) return $this->_selectLocalFields;
-    $this->_selectLocalFields = [];
-    $todoFetchAllCols = false; // select t.*
-    if (!empty($this->_params['cols'])) {
-      if (in_array('*', $this->_params['cols'])) $todoFetchAllCols = true;
-      $acl = $this->_acl;
-      $this->_selectLocalFields = array_filter($this->_params['cols'], function ($fieldQuery) use ($acl) {
-        if ('*' === $fieldQuery) return false;
-        list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
-        if (!empty($subFieldQuery)) return false;
-        return $acl->isLocalField($fieldName);
-      });
-    }
-    if (!empty($this->_params['@instances'])) $this->_selectLocalFields[] = '@instances';
-    if (empty($this->_selectLocalFields)) $todoFetchAllCols = true;
-    if (1 === count($this->_selectLocalFields) && in_array('@instances', $this->_selectLocalFields)) $todoFetchAllCols = true;
-    // if ($this->_acl instanceof TableAcl) {
-    //   $rawSelect = $this->_acl->getDataSource()->_getSqlCols();
-    //   DBG::log($rawSelect, 'string', "DBG raw select");
-    //   if ('*' !== $rawSelect && 't.*' !== $rawSelect) {
-    //     $this->_selectLocalFields['rawSelect'] = $rawSelect;
-    //   }
-    // }
-    if (!empty($this->_params['cols'])) DBG::log($this->_params['cols'], 'array', '$this->_params[cols]');
-
-    if ($todoFetchAllCols) {
-      // $this->_selectLocalFields[] = '*'; // TODO: select all $this->from local fields
-      DBG::log($this->_acl->getLocalFieldList(), 'array', "\$this->_acl->getLocalFieldList()");
-      foreach ($this->_acl->getLocalFieldList() as $localFieldName) {
-        $this->_selectLocalFields[] = $localFieldName;
-      }
-    }
-
-    $primaryKey = $this->getAclSqlPrimaryKeyField();
-    if (!in_array($primaryKey, $this->_selectLocalFields)) {
-      $this->_selectLocalFields[] = $primaryKey;
-    }
-
-    DBG::log($this->_selectLocalFields, 'array', '$this->_selectLocalFields');
-    return $this->_selectLocalFields;
-  }
-  public function getSelectRemote() { // @returns [ $fieldName => [ $fieldName, ... ] ]
-    if (null !== $this->_selectRemote) return $this->_selectRemote;
-    $this->_selectRemote = [];
-    if (!empty($this->_params['cols'])) {
-      $cols = $this->_params['cols'];
-      if (in_array('*', $cols)) {
-        $this->_selectLocalFields[] = '*';
-        $cols = array_filter($cols, function ($fieldQuery) { return '*' !== $fieldQuery; });
-      }
-      $acl = $this->_acl;
-      $cols = array_filter($cols, function ($fieldQuery) use ($acl) {
-        list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
-        // if (empty($subFieldQuery)) return false;
-        return !$acl->isLocalField($fieldName);
-      });
-      foreach ($cols as $fieldQuery) { // group by fieldName
-        list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
-        if (!array_key_exists($fieldName, $this->_selectRemote)) $this->_selectRemote[$fieldName] = [];
-        if (!empty($subFieldQuery)) $this->_selectRemote[$fieldName][] = $subFieldQuery;
-      }
-    }
-    return $this->_selectRemote;
-  }
-
-  public function fetchRowsRefs($rows) {
-    return array_map([ $this, 'fetchRowRefs' ], $rows);
-    // if (!empty($rows) && !empty($rows[0])) {
-    //   $rows[0]['default_db__x3A__CRM_WSKAZNIK:CRM_WSKAZNIK'] = [
-    //     [ 'xlink' => 'default_db__x3A__CRM_WSKAZNIK:CRM_WSKAZNIK.999' ],
-    //     [ 'xlink' => 'default_db__x3A__CRM_WSKAZNIK:CRM_WSKAZNIK.998' ],
-    //   ];
-    // }
-    return $rows;
-  }
+	public $_params;
+	public $_acl;
+	public $_query;
+	public $_total;
+	public $_legacyMode;
+	public $_selectLocalFields; // TODO: leave here or move to AclQueryBuilder? use join for perf?
+	public $_selectRemote; // TODO: ...
+
+	public function __construct($acl, $params, $legacyMode = false) {
+		$this->_acl = $acl;
+		$this->_params = $params;
+		$this->_query = null;
+		$this->_total = null;
+		$this->_legacyMode = $legacyMode;
+		// TODO: _legacyMode = ($from instanceof simple schema or another programmed objects)
+		$this->_selectLocalFields = null;
+		$this->_selectRemote = null;
+	}
+
+	public function parseQueryValue($fieldName, $searchQuery, $fieldType = 'xsd:string') {
+		if ('!NULL' === $searchQuery) return ['is not null', null];
+		if ('IS NOT NULL' === $searchQuery) return ['is not null', null];
+		if ('NULL' === $searchQuery) return ['is null', null];
+		if ('IS NULL' === $searchQuery) return ['is null', null];
+		switch ($fieldType) {
+			case 'gml:PolygonPropertyType':
+			case 'gml:PointPropertyType':
+			case 'gml:LineStringPropertyType':
+			case 'gml:GeometryPropertyType': return $this->_parseGeomQuery($searchQuery);
+			//	 $sqlFilter = $this->_sqlValueForGeomField($fldName, $v, 't');
+			// if ('_CSV_NUM' == substr($fldName, -8)) { // if ($this->isCsvNumericField($fldName)) { // TODO: xsd type - p5:csv_num
+			//	 $sqlFilter = $this->_sqlValueForCsvNumericField($fldName, $v, 't');
+			//	 if ($sqlFilter) $sql_where_and[] = $sqlFilter;
+			//	 continue;
+			// }
+		}
+
+		switch (substr($searchQuery, 0, 1)) {
+			case '=': return ['=', substr($searchQuery, 1)];
+			case '>':
+				switch (substr($searchQuery, 1, 1)) {
+					case '=': return ['>=', substr($searchQuery, 2)];
+					default:	return ['>', substr($searchQuery, 1)];
+				}
+			case '<':
+				switch (substr($searchQuery, 1, 1)) {
+					case '=': return ['<=', substr($searchQuery, 2)];
+					case '>': return ['!=', substr($searchQuery, 2)];
+					default:	return ['<', substr($searchQuery, 1)];
+				}
+			case '!':
+				switch (substr($searchQuery, 1, 1)) {
+					case '=': return ['!=', substr($searchQuery, 2)];
+					default:	return ['not like', substr($searchQuery, 1)];
+				}
+			default: {
+				switch ($fieldType) {
+					case 'xsd:number':
+					case 'xsd:int':
+					case 'xsd:integer': {
+						if (false !== strpos($searchQuery, '%')) return ['like', $searchQuery];
+						return ['=', $searchQuery];
+					}
+					default: {
+						if (false !== strpos($searchQuery, '%')) return ['like', $searchQuery];
+						$queryWhereBuilder = new SqlQueryWhereBuilder();
+						return ['and'
+							, array_map(function ($word) use ($fieldName) {
+									return [$fieldName, 'like', "%{$word}%"];
+								}, $queryWhereBuilder->splitQueryToWords($searchQuery)
+							)
+						];
+					}
+				}
+				return ['=', $searchQuery];
+			}
+		}
+	}
+	public function _parseGeomQuery($searchQuery) { // _sqlValueForGeomField($fldName, $fltrValue, $tblPrefix = 't')
+		// example: BBOX:54.40993961633866,18.583889010112824,54.337945760687454,18.397121431987586
+		DBG::log($searchQuery, 'string', "\$searchQuery");
+		if ('BBOX:' == substr($searchQuery, 0, 5)) {
+			$valParts = explode(',', substr($searchQuery, 5));
+			if (4 !== count($valParts)) throw new Exception("Wrong BBOX query");
+			$valParts = array_filter($valParts, 'is_numeric');
+			if (4 !== count($valParts)) throw new Exception("Wrong BBOX query - expected 4 numeric values");
+			$bounds = "POLYGON((
+				{$valParts[3]} {$valParts[2]},
+				{$valParts[3]} {$valParts[0]},
+				{$valParts[1]} {$valParts[0]},
+				{$valParts[1]} {$valParts[2]},
+				{$valParts[3]} {$valParts[2]}
+			))";
+			// for mysql 5.6 use ST_Contains() @see http://dev.mysql.com/doc/refman/5.6/en/spatial-relation-functions.html
+			return [ 'Intersects', $bounds ];
+		}
+		else if ('GeometryType=' == substr($fltrValue, 0, 13)) {
+			return [ 'GeometryType', substr($fltrValue, 13) ];
+		}
+		throw new Exception("Not implemented geometry query string"); // TODO:? return null;
+	}
+
+	public function _sqlValueForCsvNumericField($fldName, $fltrValue, $tblPrefix = 't') {
+		$sqlFilter = false;
+		if (is_numeric($fltrValue)) {
+			$sqlFilter = "FIND_IN_SET('{$fltrValue}', `{$fldName}`)>0";
+		} else if (false !== strpos($fltrValue, ' ')) {
+			$sqlGlue = " or ";
+			$fltrValues = $fltrValue;
+			if ('&' == substr($fltrValues, 0, 1)) {
+				$fltrValues = substr($fltrValues, 1);
+				$sqlGlue = " and ";
+			}
+			$fltrValues = explode(' ', $fltrValues);
+			$sqlNumericValues = array();
+			foreach ($fltrValues as $fltrVal) {
+				if (is_numeric($fltrVal)) {
+					$sqlNumericValues[] = "FIND_IN_SET('{$fltrVal}', `{$fldName}`)>0";
+				}
+			}
+			if (!empty($sqlNumericValues)) {
+				$sqlFilter = "(" . implode($sqlGlue, $sqlNumericValues) . ")";
+			}
+		}
+		return $sqlFilter;
+	}
+
+	public function parseSpecialFilterMsgs($type) {
+		$rootTableName = $this->_acl->getRootTableName();
+		DBG::log($rootTableName, 'string', "parse SpecialFilter Msgs({$type}), \$rootTableName");
+		$sqlHasFltrMsgs = "
+			select 1
+			from `CRM_UI_MSGS` m
+			where m.`uiTargetName`=CONCAT('{$rootTableName}.', t.`ID`)
+				and m.`uiTargetType`='default_db_table_record'
+				and m.`A_STATUS` not in('DELETED')
+			limit 1
+		";
+		switch ($type) {
+			case 'HAS_MSGS': return " ({$sqlHasFltrMsgs})=1 ";
+			case 'NO_MSGS':	return " ({$sqlHasFltrMsgs}) is null ";
+			case 'NEW_MSGS': {
+				$sqlNewFltrMsgs = "
+					select 1
+					from `CRM_UI_MSGS` m
+					where m.`uiTargetName`=CONCAT('{$rootTableName}.', t.`ID`)
+						and m.`uiTargetType`='default_db_table_record'
+						and m.`A_STATUS` in('WAITING')
+					limit 1
+				";
+				return " ({$sqlNewFltrMsgs})=1 ";
+			}
+		}
+		return null;
+	}
+
+	public function parseSpecialFilterProblemy($type) {
+		DBG::log($type, 'string', "parse SpecialFilter Problemy");
+		switch ($type) {
+			case 'PROBLEM': return ['A_PROBLEM', '!=', ''];
+			case 'WARNING': return ['A_PROBLEM', '=', 'WARNING'];
+			case 'NORMAL':	return ['A_PROBLEM', '=', 'NORMAL'];
+		}
+		return null;
+	}
+
+	public function parseSpecialFilterStatus($type) {
+		DBG::log($type, 'string', "parse SpecialFilter Status");
+		switch ($type) {
+			case 'WAITING': return ['A_STATUS', '=', 'WAITING'];
+			case 'AKTYWNI': return ['A_STATUS', 'or', [ // `A_STATUS` in('NORMAL', 'WARNING') ";
+				['A_STATUS', '=', 'NORMAL'],
+				['A_STATUS', '=', 'WARNING'],
+			] ];
+		}
+		return null;
+	}
+
+	public function parseSpecialFilterSpotkania($type) {
+		DBG::log($type, 'string', "parse SpecialFilter Spotkania");
+		switch ($type) {
+			case 'OLD': return ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN_NOW'];
+			//					 COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < UNIX_TIMESTAMP()
+			//					 and t.`L_APPOITMENT_DATE` != ''
+			//					 and t.`L_APPOITMENT_DATE` != '0000-00-00 00:00:00'
+			case 'NOW': return ['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_NOW_3600'];
+			//					 COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < UNIX_TIMESTAMP()+3600
+			//					 and COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) > UNIX_TIMESTAMP()-3600
+			case 'TODAY': return ['L_APPOITMENT_DATE', 'and', [
+				['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d"),		 date("Y"))],
+				['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN',		mktime(0,0,0, date("m"), date("d") + 1, date("Y"))],
+			] ];
+			//				 $start = mktime(0,0,0, date("m"), date("d"), date("Y"));
+			//				 $end = mktime(0,0,0, date("m"), date("d") + 1, date("Y"));
+			//				 $sqlFltr = "
+			//					 COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) > '{$start}'
+			//					 and COALESCE(UNIX_TIMESTAMP(t.`L_APPOITMENT_DATE`), 0) < '{$end}'
+			//				 ";
+			case 'TOMORROW': return ['L_APPOITMENT_DATE', 'and', [
+				['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d") + 1, date("Y"))],
+				['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN',		mktime(0,0,0, date("m"), date("d") + 2, date("Y"))],
+			] ];
+			case 'YESTERDAY': return ['L_APPOITMENT_DATE', 'and', [
+				['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_GREATER_THAN', mktime(0,0,0, date("m"), date("d") - 2, date("Y"))],
+				['L_APPOITMENT_DATE', 'UNIX_TIMESTAMP_LESS_THAN',		mktime(0,0,0, date("m"), date("d") - 1, date("Y"))],
+			] ];
+			case 'BRAK': return ['L_APPOITMENT_DATE', 'or', [
+				['L_APPOITMENT_DATE', '=', ''],
+				['L_APPOITMENT_DATE', '=', '0000-00-00 00:00:00'],
+			] ];
+		}
+		return null;
+	}
+
+	public function parseSpecialFilterAccess() {
+		$userLogin = User::getLogin();
+		$usrAclGroups = User::getLdapGroupsNames();
+		DBG::log(['login'=>$userLogin, 'groups'=>$usrAclGroups, 'hasFieldWrite'=>$this->_acl->hasField('A_ADM_COMPANY'), 'hasFieldRead'=>$this->_acl->hasField('A_CLASSIFIED'), 'acl'=>$this->_acl], 'array', "parse SpecialFilter Access");
+		$orWhere = [];
+		if ($this->_acl->hasField('A_ADM_COMPANY')) {
+			$orWhere[] = ['A_ADM_COMPANY', '=', ''];// TODO: allow empty for everyone?
+			foreach ($usrAclGroups as $group) $orWhere[] = ['A_ADM_COMPANY', '=', $group];
+		}
+		if ($this->_acl->hasField('A_CLASSIFIED')) {
+			$orWhere[] = ['A_CLASSIFIED', '=', ''];// TODO: allow empty for everyone?
+			foreach ($usrAclGroups as $group) $orWhere[] = ['A_CLASSIFIED', '=', $group];
+		}
+		if (!empty($orWhere) && $this->_acl->hasField('L_APPOITMENT_USER')) {
+			$orWhere[] = ['L_APPOITMENT_USER', '=', $userLogin];
+		}
+		return (!empty($orWhere)) ? [null, 'or', $orWhere] : null;
+	}
+
+	public function parseOgcFilter($ogcFilter) {
+		$parser = new ParseOgcFilter();
+		$parser->loadOgcFilter($ogcFilter);
+		$queryWhereBuilder = $parser->convertToSqlQueryWhereBuilder();
+		return $queryWhereBuilder->getQueryWhere('t'); // TODO: $this->_fromPrefix
+	}
+
+	public function getQuery() {
+		if ($this->_query) return clone($this->_query);
+		// $ds = $this->_acl->getDataSource(); // TODO: only for TableAcl // TODO: move _parseSqlWhere to this class
+		$filtrIsInstance = []; // $filtrIsInstance = [ $this->_acl->getNamespace() ];
+		$filtrIsNotInstance = [];
+		if (!empty($this->_params['f_is_instance'])) $filtrIsInstance = $this->_params['f_is_instance'];
+		if (!empty($this->_params['f_is_not_instance'])) $filtrIsNotInstance = $this->_params['f_is_not_instance'];
+
+		$this->_query = ACL::query($this->_acl)
+			->isInstance($filtrIsInstance)
+			->isNotInstance($filtrIsNotInstance);
+		// ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPrimaryKey} and i.idInstance = {$idInstance}" ])
+		// $this->_query->where($ds->_parseSqlWhere($params))
+		DBG::log($this->_params, 'array', "AclQueryFeatures::getQuery \$this->_params");
+		foreach ($this->_params as $k => $v) {
+			// DBG::log(['v'=>$v, 'is_numeric' => is_numeric($k), 'is_int' => is_int($k), 'is_array' => is_array($v)], 'array', "AclQueryFeatures::getQuery \$this->_params[{$k}]");
+			if (is_int($k) && is_array($v)) {
+				$this->_query->where($v); // TODO: check format [$fieldName, $comparisonSign, $value]
+			} else if (is_int($k) && null === $v) { // skip NULL
+			} else if ('f_is_instance' === $k) { // parsed before
+			} else if ('f_is_not_instance' === $k) { // parsed before
+			} else if ('@instances' === $k) { // skip - select
+			} else if ('cols' === $k) { // skip - select
+			} else if ('f_' === substr($k, 0, 2) && is_string($v) && strlen($k) > 3) {
+				$fieldName = substr($k, 2);
+				$fieldType = $this->_acl->getXsdFieldType($fieldName);
+				list($comparisonSign, $value) = $this->parseQueryValue($fieldName, $v, $fieldType);
+				DBG::log([ $fieldName, $comparisonSign, $value, $fieldType ], 'array', "parseQueryValue");
+				$this->_query->where([$fieldName, $comparisonSign, $value]);
+			} else if ('sf_' === substr($k, 0, 3) && is_string($v) && strlen($k) > 4) {
+				switch (substr($k, 3)) {
+					case 'Msgs': $this->_query->where($this->parseSpecialFilterMsgs($v)); break;
+					case 'Problemy': $this->_query->where($this->parseSpecialFilterProblemy($v)); break;
+					case 'Status': $this->_query->where($this->parseSpecialFilterStatus($v)); break;
+					case 'Spotkania': $this->_query->where($this->parseSpecialFilterSpotkania($v)); break;
+					case 'Access': break; // SKIP - used below
+					default: throw new Exception("Not Implemented special filter '".substr($k, 3)."'");
+				}
+			} else if ('ogc:Filter' === $k) {
+				$this->_query->where($this->parseOgcFilter($v));
+			} else if ('primaryKey' === $k) {
+				$fieldName = $this->_acl->getPrimaryKeyField();
+				$fieldType = $this->_acl->getXsdFieldType($fieldName);
+				list($comparisonSign, $value) = $this->parseQueryValue($fieldName, $v, $fieldType);
+				DBG::log([ $fieldName, $comparisonSign, $value, $fieldType ], 'array', "parseQueryValue");
+				$this->_query->where([$fieldName, $comparisonSign, $value]);
+			} else if ('__backRef' === $k) { // skip - parse below
+			} else if ('limit' === $k) {
+			} else if ('limitstart' === $k) {
+			} else if ('order_by' === $k) {
+			} else if ('order_dir' === $k) {
+			} else if ('sortBy' === $k) {
+			} else {
+				throw new Exception("Not Implemented param '{$k}' = '{$v}'");
+			}
+		}
+
+		// sf_Access: if 'SHOW' then show all rows, but data with ***
+		if ('SHOW' !== V::get('sf_Access', '', $this->_params)) $this->_query->where($this->parseSpecialFilterAccess());
+
+		if (array_key_exists('__backRef', $this->_params)) {
+			$backRef = $this->_params['__backRef'];
+			if (!is_array($backRef)) throw new Exception("Wrong back ref structure - expected array");
+			if (empty($backRef['namespace'])) throw new Exception("Wrong back ref structure - missing namespace");
+			if (empty($backRef['primaryKey'])) throw new Exception("Wrong back ref structure - missing primaryKey");
+			if (empty($backRef['fieldName'])) throw new Exception("Wrong back ref structure - missing fieldName");
+			// TODO: $this->_query->where([ '__backRef' ]); or	$this->_query->join([ '__backRef' ]);
+			$refAcl = ACL::getAclByNamespace($backRef['namespace']);
+			if ($refAcl->getSourceName() !== $this->_acl->getSourceName()) throw new Exception("Not implemented join with different source");
+			$refTable = ACL::getRefTable($refAcl->getNamespace(), $backRef['fieldName']);
+			// TODO: 'in' operator? // $this->_query->where($pkField, 'in', "");
+			$sqlPk = $this->getAclSqlPrimaryKeyField();
+			$sqlBackRefPk = DB::getPDO()->quote($backRef['primaryKey']);
+			$this->_query->where("
+				t.{$sqlPk} in (
+					select refTable.REMOTE_PRIMARY_KEY
+					from `{$refTable}` refTable
+					where refTable.PRIMARY_KEY = {$sqlBackRefPk}
+				)
+			");
+		}
+
+		return clone($this->_query);
+	}
+
+	public function getTotal() {
+		if ($this->_legacyMode) return $this->_acl->getTotal($this->_params);
+		if (null !== $this->_total) return $this->_total;
+		$this->_total = $this->getQuery()->fetchTotal();
+		return $this->_total;
+	}
+
+	public function hasParam($key) { return !empty($this->_params[$key]); }
+	public function getParam($key) { return V::get($key, '', $this->_params); }
+
+	public function getItems() {
+		if ($this->_legacyMode) return $this->_acl->getItems($this->_params);// TODO: array_map( $r => (array)$r )
+		// 'limit' => 10,
+		//	'limitstart' => 0,
+		//	'order_by' => 'ID',
+		//	'order_dir' => 'desc',
+		// TODO: sortBy from wfs query
+		$sortBy = ($this->hasParam('sortBy')) ? $this->getParam('sortBy') : null;
+		if (!$sortBy) {
+			$sortBy = $this->hasParam('order_by')
+				? ( $this->hasParam('order_dir')
+						? $this->getParam('order_by') . " " . $this->getParam('order_dir')
+						: $this->getParam('order_by')
+					)
+				: '';
+		}
+		$limit = V::get('limit', 10, $this->_params, 'int');
+		$offset = V::get('limitstart', 0, $this->_params, 'int');
+
+		DBG::log(['params' => $this->_params, 'sortBy' => $sortBy, 'limit' => $limit, 'offset' => $offset], 'array', '$this->_params');
+
+		$select = $this->prepareSelect();
+		DBG::log($select, 'array', "\$select is(TableAcl)=(".($this->_acl instanceof TableAcl).")");
+		DBG::log($this->getQuery(), 'array', "\$select is(TableAcl)=(".($this->_acl instanceof TableAcl).") \$this->getQuery()");
+
+		return $this->fetchRowsRefs(
+			$this->getQuery()
+				->select($select)
+				->limit($limit)
+				->offset($offset)
+				->orderBy($sortBy)
+				->fetchAll()
+		);
+	}
+
+	public function getItem($primaryKey) { // TODO: throw exception if not found?
+		if ($this->_legacyMode) return (array)$this->_acl->getItem($primaryKey, $this->_params);
+		$select = $this->prepareSelect();
+		$pkField = $this->_acl->getPrimaryKeyField();
+		return $this->fetchRowRefs(
+			$this->getQuery()
+				->select($select)
+				->where([$pkField, '=', $primaryKey])
+				->fetchFirst()
+		);
+	}
+
+	public function prepareSelect() { // TODO: replace with getSelectLocal
+		// TODO: select from params: 'cols' => [ fieldName, ... ]
+		// TODO: select from params: '@instances' => 1
+		// TODO: if no fields set, then '*'
+		// TODO: select must contain primaryKey
+		return $this->getSelectLocal();
+	}
+	public function getSelectLocal() { // @returns [ $fieldName, ... ]
+		// TODO: rawSelect!
+		if (null !== $this->_selectLocalFields) return $this->_selectLocalFields;
+		$this->_selectLocalFields = [];
+		$todoFetchAllCols = false; // select t.*
+		if (!empty($this->_params['cols'])) {
+			if (in_array('*', $this->_params['cols'])) $todoFetchAllCols = true;
+			$acl = $this->_acl;
+			$this->_selectLocalFields = array_filter($this->_params['cols'], function ($fieldQuery) use ($acl) {
+				if ('*' === $fieldQuery) return false;
+				list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
+				if (!empty($subFieldQuery)) return false;
+				return $acl->isLocalField($fieldName);
+			});
+		}
+		if (!empty($this->_params['@instances'])) $this->_selectLocalFields[] = '@instances';
+		if (empty($this->_selectLocalFields)) $todoFetchAllCols = true;
+		if (1 === count($this->_selectLocalFields) && in_array('@instances', $this->_selectLocalFields)) $todoFetchAllCols = true;
+		// if ($this->_acl instanceof TableAcl) {
+		//	 $rawSelect = $this->_acl->getDataSource()->_getSqlCols();
+		//	 DBG::log($rawSelect, 'string', "DBG raw select");
+		//	 if ('*' !== $rawSelect && 't.*' !== $rawSelect) {
+		//		 $this->_selectLocalFields['rawSelect'] = $rawSelect;
+		//	 }
+		// }
+		if (!empty($this->_params['cols'])) DBG::log($this->_params['cols'], 'array', '$this->_params[cols]');
+
+		if ($todoFetchAllCols) {
+			// $this->_selectLocalFields[] = '*'; // TODO: select all $this->from local fields
+			DBG::log($this->_acl->getLocalFieldList(), 'array', "\$this->_acl->getLocalFieldList()");
+			foreach ($this->_acl->getLocalFieldList() as $localFieldName) {
+				$this->_selectLocalFields[] = $localFieldName;
+			}
+		}
+
+		$primaryKey = $this->getAclSqlPrimaryKeyField();
+		if (!in_array($primaryKey, $this->_selectLocalFields)) {
+			$this->_selectLocalFields[] = $primaryKey;
+		}
+
+		DBG::log($this->_selectLocalFields, 'array', '$this->_selectLocalFields');
+		return $this->_selectLocalFields;
+	}
+	public function getSelectRemote() { // @returns [ $fieldName => [ $fieldName, ... ] ]
+		if (null !== $this->_selectRemote) return $this->_selectRemote;
+		$this->_selectRemote = [];
+		if (!empty($this->_params['cols'])) {
+			$cols = $this->_params['cols'];
+			if (in_array('*', $cols)) {
+				$this->_selectLocalFields[] = '*';
+				$cols = array_filter($cols, function ($fieldQuery) { return '*' !== $fieldQuery; });
+			}
+			$acl = $this->_acl;
+			$cols = array_filter($cols, function ($fieldQuery) use ($acl) {
+				list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
+				// if (empty($subFieldQuery)) return false;
+				return !$acl->isLocalField($fieldName);
+			});
+			foreach ($cols as $fieldQuery) { // group by fieldName
+				list($fieldName, $subFieldQuery) = explode('/', $fieldQuery, 2);
+				if (!array_key_exists($fieldName, $this->_selectRemote)) $this->_selectRemote[$fieldName] = [];
+				if (!empty($subFieldQuery)) $this->_selectRemote[$fieldName][] = $subFieldQuery;
+			}
+		}
+		return $this->_selectRemote;
+	}
+
+	public function fetchRowsRefs($rows) {
+		return array_map([ $this, 'fetchRowRefs' ], $rows);
+		// if (!empty($rows) && !empty($rows[0])) {
+		//	 $rows[0]['default_db__x3A__CRM_WSKAZNIK:CRM_WSKAZNIK'] = [
+		//		 [ 'xlink' => 'default_db__x3A__CRM_WSKAZNIK:CRM_WSKAZNIK.999' ],
+		//		 [ 'xlink' => 'default_db__x3A__CRM_WSKAZNIK:CRM_WSKAZNIK.998' ],
+		//	 ];
+		// }
+		return $rows;
+	}
 
 
 	public function fetchRowRefs($row) {
 	public function fetchRowRefs($row) {
 		if (!$row) return $row;
 		if (!$row) return $row;
@@ -530,10 +530,10 @@ class AclQueryFeatures {
 		return $row;
 		return $row;
 	}
 	}
 
 
-  public function getAclSqlPrimaryKeyField() {
-    return ($this->_acl instanceof Core_AclBase)
-      ? $this->_acl->getSqlPrimaryKeyField()
-      : 'ID';
-  }
+	public function getAclSqlPrimaryKeyField() {
+		return ($this->_acl instanceof Core_AclBase)
+			? $this->_acl->getSqlPrimaryKeyField()
+			: 'ID';
+	}
 
 
 }
 }