INsensitive search - Oracle
authorFilip Visic <visicfilip@gmail.com>
Tue, 21 May 2013 13:29:42 +0000 (15:29 +0200)
committerPhilipp Schüle <p.schuele@metaways.de>
Mon, 3 Jun 2013 09:17:18 +0000 (11:17 +0200)
Change-Id: Ie2dd59f2fbd33b6eaa21c42dd9a45dcc3b0b5903
Reviewed-on: https://gerrit.tine20.org/tine20/2043
Tested-by: jenkins user
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
tests/tine20/Addressbook/Backend/SqlTest.php
tine20/Tinebase/Backend/Sql/Command/Interface.php
tine20/Tinebase/Backend/Sql/Command/Mysql.php
tine20/Tinebase/Backend/Sql/Command/Oracle.php
tine20/Tinebase/Backend/Sql/Command/Pgsql.php
tine20/Tinebase/Model/Filter/Query.php
tine20/Tinebase/Model/Filter/Text.php
tine20/Tinebase/User/Sql.php
tine20/Tinebase/js/widgets/grid/FilterModel.js

index c6dd541..8489813 100644 (file)
@@ -96,6 +96,20 @@ class Addressbook_Backend_SqlTest extends PHPUnit_Framework_TestCase
         
         return $contact;
     }
+    
+    /**
+     * try to add a contact
+     * 
+     * @return Addressbook_Model_Contact
+     */
+    public function testCreateSpecialNameContact()
+    {
+        $contact = $this->_backend->create(self::getTestSpecialNameContact($this->_container));
+        
+        $this->assertTrue(!empty($contact->id));
+        
+        return $contact;
+    }    
 
     /**
      * try to get a contact
@@ -163,6 +177,36 @@ class Addressbook_Backend_SqlTest extends PHPUnit_Framework_TestCase
     }
     
     /**
+     * test search results
+     * 
+     * @return Addressbook_Model_Contact
+     */
+    public function testSearchSpecialNameContact()
+    {
+        $contact = $this->testCreateSpecialNameContact();
+        
+        $filter = new Addressbook_Model_ContactFilter(array(
+            array(
+                'field' => 'container_id',     'operator' => 'equals',         'value' => $contact->container_id,
+                'field' => 'n_family',         'operator' => 'equalsspecial',  'value' => 'Horvat-Čuka'
+            )
+        ));
+        
+        $contacts = $this->_backend->search($filter);
+        $this->assertTrue(count($contacts) >= 1, 'empty search');
+        
+        $filter = new Addressbook_Model_ContactFilter(array(
+            array(
+                'field' => 'container_id',     'operator' => 'equals',         'value' => $contact->container_id,
+                'field' => 'n_given',          'operator' => 'equalsspecial',  'value' => 'Ana Maria'
+            )
+        ));
+        
+        $contacts = $this->_backend->search($filter);
+        $this->assertTrue(count($contacts) >= 1, 'empty search');
+    }    
+    
+    /**
      * test if image is in contact
      * 
      * 
@@ -279,10 +323,61 @@ class Addressbook_Backend_SqlTest extends PHPUnit_Framework_TestCase
         ));
 
         return $contact;
+    }     
+    /**
+     * create test contact
+     * 
+     * @return Addressbook_Model_Contact
+     */
+    public static function getTestSpecialNameContact(Tinebase_Model_Container $container)
+    {
+        $contact = new Addressbook_Model_Contact(array(
+            'adr_one_countryname'   => 'HR',
+            'adr_one_locality'      => 'Šibenik',
+            'adr_one_postalcode'    => '2200',
+            'adr_one_region'        => 'Bilice',
+            'adr_one_street'        => 'Snajperska 4',
+            'adr_one_street2'       => 'no second street',
+            'adr_two_countryname'   => 'HR',
+            'adr_two_locality'      => 'Zagreb',
+            'adr_two_postalcode'    => '10xxx',
+            'adr_two_region'        => 'Zagreb',
+            'adr_two_street'        => 'Pick 4',
+            'adr_two_street2'       => 'no second street2',
+            'assistent'             => 'Cornelius Weiß',
+            'bday'                  => '1975-01-02 03:04:05', // new Tinebase_DateTime???
+            'email'                 => 'unittests2@tine20.org',
+            'email_home'            => 'unittests2@tine20.org',
+            'jpegphoto'             => '',
+            'note'                  => 'Bla Bla Bla',
+            'container_id'          => $container->id,
+            'role'                  => 'Role',
+            'title'                 => 'Title',
+            'url'                   => 'http://www.tine20.org',
+            'url_home'              => 'http://www.tine20.com',
+            'n_family'              => 'Horvat Čuka',
+            'n_fileas'              => 'Horvat Čuka, Ana-Maria',
+            'n_given'               => 'Ana-Maria',
+            'n_middle'              => 'no middle name',
+            'n_prefix'              => 'no prefix',
+            'n_suffix'              => 'no suffix',
+            'org_name'              => 'Metaways Infosystems GmbH',
+            'org_unit'              => 'Tine 2.0',
+            'tel_assistent'         => '+385TELASSISTENT',
+            'tel_car'               => '+385TELCAR',
+            'tel_cell'              => '+385TELCELL',
+            'tel_cell_private'      => '+385TELCELLPRIVATE',
+            'tel_fax'               => '+385TELFAX',
+            'tel_fax_home'          => '+385TELFAXHOME',
+            'tel_home'              => '+385TELHOME',
+            'tel_pager'             => '+385TELPAGER',
+            'tel_work'              => '+385TELWORK',
+        ));
+        return $contact;
     }
 }        
 
-
 if (PHPUnit_MAIN_METHOD == 'Addressbook_Backend_SqlTest::main') {
     Addressbook_Backend_SqlTest::main();
 }
index 6c60491..ae76324 100755 (executable)
@@ -75,6 +75,14 @@ interface Tinebase_Backend_Sql_Command_Interface
     public function getLike();
     
     /**
+     * prepare value for case insensitive search
+     * 
+     * @param string $value
+     * @return string
+     */
+    public function prepareForILike($value);
+    
+    /**
      * returns field without accents (diacritic signs) - for Pgsql;
      * 
      * @param string $field
index 48b4441..4d8df58 100644 (file)
@@ -119,6 +119,17 @@ class Tinebase_Backend_Sql_Command_Mysql implements Tinebase_Backend_Sql_Command
     }
     
     /**
+     * prepare value for case insensitive search
+     * 
+     * @param string $value
+     * @return string
+     */
+    public function prepareForILike($value)
+    {
+        return $value;
+    }
+    
+    /**
      * returns field without accents (diacritic signs) - for Pgsql;
      * 
      * @param string $field
index 11210ff..475ad7d 100755 (executable)
@@ -119,6 +119,17 @@ class Tinebase_Backend_Sql_Command_Oracle implements Tinebase_Backend_Sql_Comman
     }
     
     /**
+     * prepare value for case insensitive search
+     * 
+     * @param string $value
+     * @return string
+     */
+    public function prepareForILike($value)
+    {
+        return ' UPPER (' . $value . ')';
+    }
+    
+    /**
      * returns field without accents (diacritic signs) - for Pgsql;
      * 
      * @param string $field
index b74fec3..d2b040c 100755 (executable)
@@ -125,6 +125,17 @@ class Tinebase_Backend_Sql_Command_Pgsql implements Tinebase_Backend_Sql_Command
     }
     
     /**
+     * prepare value for case insensitive search
+     * 
+     * @param string $value
+     * @return string
+     */
+    public function prepareForILike($value)
+    {
+        return $value;
+    }
+    
+    /**
      * Even if the database backend is PostgreSQL, we have to verify
      *  if the extension Unaccent is installed and loaded. 
      *  This is done in Tinebase_Core::checkUnaccentExtension.
index 551e2bb..824419d 100644 (file)
@@ -34,7 +34,7 @@ class Tinebase_Model_Filter_Query extends Tinebase_Model_Filter_Abstract
         2 => 'equals',
         3 => 'startswith'
     );
-    
+
     /**
      * set options 
      *
@@ -49,7 +49,7 @@ class Tinebase_Model_Filter_Query extends Tinebase_Model_Filter_Abstract
         
         $this->_options = $_options;
     }
-    
+
     /**
      * appends sql to given select statement
      *
@@ -62,9 +62,9 @@ class Tinebase_Model_Filter_Query extends Tinebase_Model_Filter_Abstract
              $_select->where('1=1/* empty query */');
              return;
          }
-        
+
          $db = Tinebase_Core::getDb();
-         
+
          switch ($this->_operator) {
              case 'contains':
              case 'equals':
@@ -73,8 +73,13 @@ class Tinebase_Model_Filter_Query extends Tinebase_Model_Filter_Abstract
                  foreach ($queries as $query) {
                      $whereParts = array();
                      foreach ($this->_options['fields'] as $qField) {
-                         $whereParts[] = Tinebase_Backend_Sql_Command::factory($db)->getUnaccent($db->quoteIdentifier($_backend->getTableName()
-                             . '.' . $qField)) . ' ' . Tinebase_Backend_Sql_Command::factory($db)->getLike() . Tinebase_Backend_Sql_Command::factory($db)->getUnaccent(' ?');
+                         // if field has . in name, then we already have tablename
+                        if (strpos($qField, '.') !== FALSE) {
+                            $whereParts[] = Tinebase_Backend_Sql_Command::factory($db)->prepareForILike(Tinebase_Backend_Sql_Command::factory($db)->getUnaccent($db->quoteIdentifier($qField))) . ' ' . Tinebase_Backend_Sql_Command::factory($db)->getLike() . Tinebase_Backend_Sql_Command::factory($db)->prepareForILike(Tinebase_Backend_Sql_Command::factory($db)->getUnaccent('(?)'));
+                        }
+                        else {
+                            $whereParts[] = Tinebase_Backend_Sql_Command::factory($db)->prepareForILike(Tinebase_Backend_Sql_Command::factory($db)->getUnaccent($db->quoteIdentifier($_backend->getTableName() . '.' . $qField))) . ' ' . Tinebase_Backend_Sql_Command::factory($db)->getLike() . Tinebase_Backend_Sql_Command::factory($db)->prepareForILike(Tinebase_Backend_Sql_Command::factory($db)->getUnaccent('(?)'));
+                        }
                      }
                      $whereClause = '';
                      if (!empty($whereParts)) {
@@ -96,7 +101,13 @@ class Tinebase_Model_Filter_Query extends Tinebase_Model_Filter_Abstract
                  break;
              case 'in':
                  foreach ($this->_options['fields'] as $qField) {
-                     $whereParts[] = $db->quoteInto($db->quoteIdentifier($_backend->getTableName() . '.' . $qField) . ' IN (?)', (array) $this->_value);
+                    // if field has . in name, then we allready have tablename
+                    if (strpos($qField, '.') !== FALSE) {
+                        $whereParts[] = $db->quoteInto($db->quoteIdentifier($qField) . ' IN (?)', (array) $this->_value);
+                    }
+                    else {
+                         $whereParts[] = $db->quoteInto($db->quoteIdentifier($_backend->getTableName() . '.' . $qField) . ' IN (?)', (array) $this->_value);
+                    }
                  }
                  if (! empty($whereParts)) {
                      $whereClause = implode(' OR ', $whereParts);
@@ -109,4 +120,4 @@ class Tinebase_Model_Filter_Query extends Tinebase_Model_Filter_Abstract
                  throw new Tinebase_Exception_InvalidArgument('Operator not defined: ' . $this->_operator);
          }
      }
-}
+}
\ No newline at end of file
index 40efcb6..4e5200c 100644 (file)
@@ -32,26 +32,50 @@ class Tinebase_Model_Filter_Text extends Tinebase_Model_Filter_Abstract
         6 => 'notin',
         7 => 'isnull',
         8 => 'notnull',
-    // add 'group by _fieldname_' to select statement and remove empty values / filter value is not used when this operator is set
+        // add 'group by _fieldname_' to select statement and remove empty values / filter value is not used when this operator is set
         9 => 'group',
+
+        // filter replace space and - with %
+        10 => 'equalsspecial'
     );
     
     /**
      * @var array maps abstract operators to sql operators
+     * filled in constructor
      */
-    protected $_opSqlMap = array(
-        'equals'     => array('sqlop' => ' LIKE ?',      'wildcards' => '?'  ),
-        'contains'   => array('sqlop' => ' LIKE ?',      'wildcards' => '%?%'),
-        'startswith' => array('sqlop' => ' LIKE ?',      'wildcards' => '?%' ),
-        'endswith'   => array('sqlop' => ' LIKE ?',      'wildcards' => '%?' ),
-        'not'        => array('sqlop' => ' NOT LIKE ?',  'wildcards' => '?'  ),
-        'in'         => array('sqlop' => ' IN (?)',      'wildcards' => '?'  ),
-        'notin'      => array('sqlop' => ' NOT IN (?)',  'wildcards' => '?'  ),
-        'isnull'     => array('sqlop' => ' IS NULL',     'wildcards' => '?'  ),
-        'notnull'    => array('sqlop' => ' IS NOT NULL', 'wildcards' => '?'  ),
-        'group'      => array('sqlop' => " NOT LIKE  ''",'wildcards' => '?'  ),
-    );
-    
+    protected $_opSqlMap = array();
+
+    /**
+     * get a new single filter action
+     *
+     * @param string|array $_fieldOrData
+     * @param string $_operator
+     * @param mixed  $_value
+     * @param array  $_options
+     *
+     * @todo remove legacy code + obsolete params sometimes
+     */
+    public function __construct($_fieldOrData, $_operator = NULL, $_value = NULL, array $_options = array())
+    {
+        $db = Tinebase_Core::getDb();
+
+        $this->_opSqlMap = array(
+            'equals'            => array('sqlop' => ' LIKE ' . Tinebase_Backend_Sql_Command::factory($db)->prepareForILike('(?)'),          'wildcards' => '?'  ),
+            'contains'          => array('sqlop' => ' LIKE ' . Tinebase_Backend_Sql_Command::factory($db)->prepareForILike('(?)'),          'wildcards' => '%?%'),
+            'startswith'        => array('sqlop' => ' LIKE ' . Tinebase_Backend_Sql_Command::factory($db)->prepareForILike('(?)'),          'wildcards' => '?%' ),
+            'endswith'          => array('sqlop' => ' LIKE ' . Tinebase_Backend_Sql_Command::factory($db)->prepareForILike('(?)'),          'wildcards' => '%?' ),
+            'not'               => array('sqlop' => ' NOT LIKE ' . Tinebase_Backend_Sql_Command::factory($db)->prepareForILike('(?)'),      'wildcards' => '?'  ),
+            'in'                => array('sqlop' => ' IN (?)',          'wildcards' => '?'  ),
+            'notin'             => array('sqlop' => ' NOT IN (?)',      'wildcards' => '?'  ),
+            'isnull'            => array('sqlop' => ' IS NULL',         'wildcards' => '?'  ),
+            'notnull'           => array('sqlop' => ' IS NOT NULL',     'wildcards' => '?'  ),
+            'group'             => array('sqlop' => " NOT LIKE  ''",    'wildcards' => '?'  ),
+            'equalsspecial'     => array('sqlop' => ' LIKE ' . Tinebase_Backend_Sql_Command::factory($db)->prepareForILike('(?)'),          'wildcards' => '?'     ),
+        );
+
+        parent::__construct($_fieldOrData, $_operator, $_value, $_options);
+    }
+
     /**
      * appends sql to given select statement
      *
@@ -75,21 +99,39 @@ class Tinebase_Model_Filter_Text extends Tinebase_Model_Filter_Abstract
             $value = explode(' ', $value);
         }
         
+        $db = Tinebase_Core::getDb();
+                
         if (is_array($value) && empty($value)) {
              $_select->where('1=' . (substr($this->_operator, 0, 3) == 'not' ? '1/* empty query */' : '0/* impossible query */'));
              return;
         }
         
-        $where = Tinebase_Core::getDb()->quoteInto($field . $action['sqlop'], $value);
+        if ($this->_operator == 'equalsspecial') {
+            if (is_array($value)) {
+                foreach($value as $key => $v){
+                    $value[$key] = preg_replace('/(\s+|\-)/', '%', $v);
+                }
+            }
+            else {
+                $value = preg_replace('/(\s+|\-)/', '%', $value);
+            }  
+        }
         
+        if (! in_array($this->_operator, array('in', 'notin'))) {
+            $where = Tinebase_Core::getDb()->quoteInto(Tinebase_Backend_Sql_Command::factory($db)->prepareForILike($field) . ' ' . $action['sqlop'], $value);
+        }
+        else {
+            $where = Tinebase_Core::getDb()->quoteInto($field . $action['sqlop'], $value);
+        }
+
         if (in_array($this->_operator, array('not', 'notin')) && $value !== '') {
             $where = "( $where OR $field IS NULL)";
         }
-        
-        if (in_array($this->_operator, array('equals', 'contains', 'startswith', 'endswith', 'in')) && $value === '') {
+
+        if (in_array($this->_operator, array('equals', 'equalsspecial', 'contains', 'startswith', 'endswith', 'in')) && $value === '') {
             $where = "( $where OR $field IS NULL)";
         }
-        
+
         // finally append query to select object
         $_select->where($where);
     }
index ad436eb..eff4d2b 100644 (file)
@@ -119,8 +119,10 @@ class Tinebase_User_Sql extends Tinebase_User_Abstract
                 $this->rowNameMapping['accountFirstName'], 
                 $this->rowNameMapping['accountLoginName']
             );
+            // prepare for case insensitive search
+            $db = Tinebase_Core::getDb();
             foreach ($defaultValues as $defaultValue) {
-                $whereStatement[] = $this->_db->quoteIdentifier($defaultValue) . 'LIKE ?';
+                $whereStatement[] = Tinebase_Backend_Sql_Command::factory($db)->prepareForILike($this->_db->quoteIdentifier($defaultValue)) . ' LIKE ' . Tinebase_Backend_Sql_Command::factory($db)->prepareForILike('?');
             }
         
             $select->where('(' . implode(' OR ', $whereStatement) . ')', '%' . $_filter . '%');
@@ -932,11 +934,13 @@ class Tinebase_User_Sql extends Tinebase_User_Abstract
     /**
      * Get multiple users
      *
+     * fetch FullUser by default
+     *
      * @param     string|array $_id Ids
      * @param   string  $_accountClass  type of model to return
      * @return Tinebase_Record_RecordSet of 'Tinebase_Model_User' or 'Tinebase_Model_FullUser'
      */
-    public function getMultiple($_id, $_accountClass = 'Tinebase_Model_User') 
+    public function getMultiple($_id, $_accountClass = 'Tinebase_Model_FullUser') 
     {
         if (empty($_id)) {
             return new Tinebase_Record_RecordSet($_accountClass);
index d97917c..5621b83 100644 (file)
@@ -165,21 +165,22 @@ Ext.extend(Tine.widgets.grid.FilterModel, Ext.util.Observable, {
         var operatorStore = new Ext.data.JsonStore({
             fields: ['operator', 'label'],
             data: [
-                {operator: 'contains',   label: _('contains')},
-                {operator: 'regex',      label: _('reg. exp.')},
-                {operator: 'equals',     label: _('is equal to')},
-                {operator: 'greater',    label: _('is greater than')},
-                {operator: 'less',       label: _('is less than')},
-                {operator: 'not',        label: _('is not')},
-                {operator: 'in',         label: _('one of')},
-                {operator: 'notin',      label: _('none of')},
-                {operator: 'before',     label: _('is before')},
-                {operator: 'after',      label: _('is after')},
-                {operator: 'within',     label: _('is within')},
-                {operator: 'inweek',     label: _('is in week no.')},
-                {operator: 'startswith', label: _('starts with')},
-                {operator: 'endswith',   label: _('ends with')},
-                {operator: 'definedBy',  label: _('defined by')}
+                {operator: 'contains',      label: _('contains')},
+                {operator: 'regex',         label: _('reg. exp.')},
+                {operator: 'equals',        label: _('is equal to')},
+                {operator: 'equalsspecial', label: _('is equal to without (-, )')},
+                {operator: 'greater',       label: _('is greater than')},
+                {operator: 'less',          label: _('is less than')},
+                {operator: 'not',           label: _('is not')},
+                {operator: 'in',            label: _('one of')},
+                {operator: 'notin',         label: _('none of')},
+                {operator: 'before',        label: _('is before')},
+                {operator: 'after',         label: _('is after')},
+                {operator: 'within',        label: _('is within')},
+                {operator: 'inweek',        label: _('is in week no.')},
+                {operator: 'startswith',    label: _('starts with')},
+                {operator: 'endswith',      label: _('ends with')},
+                {operator: 'definedBy',     label: _('defined by')}
             ].concat(this.getCustomOperators() || []),
             remoteSort: false,
             sortInfo: {