[FEATURE] Integrate full-text search 42/36142/6
authorFrancois Suter <francois@typo3.org>
Tue, 20 Jan 2015 11:12:00 +0000 (12:12 +0100)
committerFrancois Suter <francois@typo3.org>
Mon, 9 Feb 2015 17:01:10 +0000 (18:01 +0100)
Adds fulltext search capabilities to dataquery.

Releases: 1.11
Resolves: #41882

Change-Id: I17b8de3ce60790d9cb397bb4596389f6468e64ca
Reviewed-on: http://review.typo3.org/36142
Reviewed-by: Francois Suter <francois@typo3.org>
Tested-by: Francois Suter <francois@typo3.org>
16 files changed:
Classes/Parser/Fulltext.php [new file with mode: 0644]
Classes/Userfunc/FormEngine.php [new file with mode: 0644]
Classes/Utility/DatabaseAnalyser.php [new file with mode: 0644]
Documentation/Images/FulltextHelpField.png [new file with mode: 0644]
Documentation/Queries/Fulltext/Index.rst [new file with mode: 0644]
Documentation/Queries/Index.rst
class.tx_dataquery_parser.php
class.tx_dataquery_queryobject.php
class.tx_dataquery_sqlparser.php
class.tx_dataquery_sqlutility.php
ext_autoload.php
ext_conf_template.txt
ext_emconf.php
locallang_db.xml
tca.php
tests/tx_dataquery_sqlbuilder_Test.php

diff --git a/Classes/Parser/Fulltext.php b/Classes/Parser/Fulltext.php
new file mode 100644 (file)
index 0000000..842c214
--- /dev/null
@@ -0,0 +1,195 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2012-2015 Fabien Udriot (Cobweb) <fudriot@cobweb.ch>
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+
+/**
+ * Provides routine to manipulate a query adding a fulltext segment in the query.
+ *
+ * @author Fabien Udriot (Cobweb) <support@cobweb.ch>
+ * @author Francois Suter (Cobweb) <support@cobweb.ch>
+ * @package TYPO3
+ * @subpackage dataquery
+ */
+class Tx_Dataquery_Parser_Fulltext {
+
+       /**
+        * @var string
+        */
+       protected $searchTerms = array();
+
+       /**
+        * @var Tx_Dataquery_Utility_DatabaseAnalyser
+        */
+       protected $analyser;
+
+       /**
+        * @var array
+        */
+       protected $indexedFields = array();
+
+       /**
+        * Unserialized extension configuration
+        * @var array
+        */
+       protected $configuration;
+
+       /**
+        * Constructor
+        *
+        * @param string $tableName: the main table name
+        * @return Tx_Dataquery_Parser_Fulltext
+        */
+       public function __construct() {
+
+               /** @var $analyser Tx_Dataquery_Utility_DatabaseAnalyser */
+               $analyser = t3lib_div::makeInstance('Tx_Dataquery_Utility_DatabaseAnalyser');
+               $this->setAnalyser($analyser);
+               $this->configuration = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['dataquery']);
+       }
+
+       /**
+        * Retrieves full-text index fields for a given table.
+        *
+        * @param string $tableName
+        */
+       protected function retrieveIndexedFields($tableName) {
+               $this->indexedFields = $this->analyser->getFields($tableName);
+       }
+
+       /**
+        * Sets the analyser.
+        *
+        * Useful for unit tests.
+        *
+        * @param Tx_Dataquery_Utility_DatabaseAnalyser $analyser
+        */
+       public function setAnalyser($analyser) {
+               $this->analyser = $analyser;
+       }
+
+       /**
+        * Sets the extension configuration.
+        *
+        * Useful for unit tests.
+        *
+        * @param array $configuration Extension configuration
+        * @return void
+        */
+       public function setConfiguration($configuration) {
+               $this->configuration = $configuration;
+       }
+
+       /**
+        * Parses the query. If a placeholder "fulltext:foo" is found, then replace with a MATCH / AGAINST expression.
+        *
+        * @param string $table Name of the table to search
+        * @param string $index Name of the fulltext index to use
+        * @param string $search Search string
+        * @param boolean $isNaturalSearch TRUE if fulltext search should be in natural mode
+        * @param boolean $isNegated TRUE if condition should be negated
+        * @return string SQL MATCH() condition
+        * @throws tx_tesseract_exception
+        */
+       public function parse($table, $index, $search, $isNaturalSearch, $isNegated) {
+               $this->retrieveIndexedFields($table);
+               if (isset($this->indexedFields[$index])) {
+                       $indexFields = $this->indexedFields[$index];
+               } else {
+                       throw new tx_tesseract_exception(
+                               sprintf('Table %s has no index "%s"', $table, $index),
+                               1421769189
+                       );
+               }
+               // Search terms from a query string will be urlencode'd
+               $processedSearchTerms = urldecode($search);
+               $booleanMode = '';
+               if (!$isNaturalSearch) {
+                       $processedSearchTerms = $this->processSearchTerm($processedSearchTerms);
+                       $booleanMode = ' IN BOOLEAN MODE';
+               }
+               if (empty($processedSearchTerms)) {
+                       throw new tx_tesseract_exception(
+                               'Empty fulltext search condition',
+                               1423068811
+                       );
+               }
+               $baseCondition = "MATCH(%s) AGAINST('%s'%s)";
+               if ($isNegated) {
+                       $baseCondition = 'NOT ' . $baseCondition;
+               }
+               $condition = sprintf($baseCondition, $indexFields, $processedSearchTerms, $booleanMode);
+               return $condition;
+       }
+
+       /**
+        * Processes the search term.
+        *
+        * @param string $term Search term
+        * @return string
+        */
+       public function processSearchTerm($term) {
+
+               $termsProcessed = array();
+
+               // Handle double quote wrapping
+               if (preg_match_all('/".+"/isU', $term, $matches)) {
+
+                       foreach ($matches as $match) {
+                               $searchedCharacters = array(
+                                       '"',
+                                       ' '
+                               );
+                               $replacedCharacters = array(
+                                       '',
+                                       '###'
+                               );
+                               $search = $match;
+                               $replace = str_replace($searchedCharacters, $replacedCharacters, $match);
+                               $term = str_replace($search, $replace, $term);
+                       }
+               }
+
+               $terms = explode(' ', $term);
+               foreach ($terms as $term) {
+                       if (!empty($term)) {
+                               // Handle exclusion of term
+                               $logic = '+';
+                               if (substr($term, 0, 1) == '-') {
+                                       $term = substr($term, 1);
+                                       $logic = '-';
+                               }
+                               if (strlen($term) >= $this->configuration['fullTextMinimumWordLength']) {
+                                       $termProcessed = str_replace('###', ' ', addslashes($term));
+                                       $termsProcessed[] = sprintf('%s"%s"', $logic, $termProcessed);
+                               }
+                       }
+               }
+               return implode(' ', $termsProcessed);
+       }
+}
+
+
+if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['dataquery/Classes/Parser/Fulltext.php'])  {
+       include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['dataquery/Classes/Parser/Fulltext.php']);
+}
+?>
\ No newline at end of file
diff --git a/Classes/Userfunc/FormEngine.php b/Classes/Userfunc/FormEngine.php
new file mode 100644 (file)
index 0000000..91a8af2
--- /dev/null
@@ -0,0 +1,150 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2012-2015 Fabien Udriot (Cobweb) <fudriot@cobweb.ch>
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+
+/**
+ * Displays a custom field in the BE to check for fulltext indices and
+ * provide useful hints.
+ *
+ * @author Fabien Udriot (Cobweb) <support@cobweb.ch>
+ * @author Francois Suter (Cobweb) <support@cobweb.ch>
+ * @package TYPO3
+ * @subpackage dataquery
+ */
+class Tx_Dataquery_Userfunc_FormEngine {
+
+       /**
+        * @var Tx_Dataquery_Utility_DatabaseAnalyser
+        */
+       protected $analyser;
+
+       /**
+        * @var tx_dataquery_sqlparser
+        */
+       protected $sqlParser;
+
+       /**
+        * @var language
+        */
+       protected $language;
+
+       /**
+        * Stores the main table of the SQL query
+        * @var string
+        */
+       protected $table;
+
+       /**
+        * Constructor
+        */
+       public function __construct() {
+               $this->language = $GLOBALS['LANG'];
+               $this->analyser = t3lib_div::makeInstance('Tx_Dataquery_Utility_DatabaseAnalyser');
+               $this->sqlParser = t3lib_div::makeInstance('tx_dataquery_sqlparser');
+       }
+
+       /**
+        * This method format a message regarding FULLTEXT indexes in the database towards a BE user.
+        *
+        * @param array $parameters Properties of the field being modified
+        * @param t3lib_TCEforms $parentObject Back-reference to the calling object
+        * @return string
+        */
+       public function renderFulltextIndices($parameters, t3lib_TCEforms $parentObject) {
+               $output = $this->language->sL('LLL:EXT:dataquery/locallang_db.xlf:fulltext.no_index_or_missing_table');
+
+               if (empty($parameters['row']['sql_query'])) {
+                       $output = $this->language->sL('LLL:EXT:dataquery/locallang_db.xlf:fulltext.no_query');
+               } else {
+
+                       // Fetch the query parts
+                       try {
+                               $query = $this->sqlParser->parseSQL($parameters['row']['sql_query']);
+
+                               if (!empty($query->structure['FROM']['table'])) {
+                                       $this->table = $query->structure['FROM']['table'];
+                                       if ($this->analyser->hasIndex($this->table)) {
+                                               $output = $this->getMessageOk();
+                                       } else {
+                                               $output = $this->getMessageNoIndexFound();
+                                       }
+                               }
+                       }
+                       catch (Exception $e) {
+                               // Nothing to do, the default message will do fine
+                       }
+               }
+               return $output;
+       }
+
+       /**
+        * Formats a message for the BE displaying all possible FULLTEXT index to the BE User.
+        *
+        * @return string
+        */
+       protected function getMessageOk() {
+               $fields = $this->analyser->getFields($this->table);
+               $output = '';
+               foreach ($fields as $index => $indexedFields) {
+                       $output .= sprintf(
+                               '%s <strong>fulltext:%s AS foo</strong>',
+                               $this->language->sL('LLL:EXT:dataquery/locallang_db.xlf:fulltext.syntax_for_query'),
+                               $index
+                       );
+                       $output .= sprintf(
+                               '<br/>' . $this->language->sL('LLL:EXT:dataquery/locallang_db.xlf:fulltext.indexed_fields') . '<br/>',
+                               $index,
+                               $indexedFields
+                       );
+                       $output = '<div style="margin-bottom: 10px">' . $output . '</div>';
+               }
+               return '<div>' . $output . '</div>';
+       }
+
+       /**
+        * Formats a message for the BE when no FULLTEXT index is found against a table.
+        *
+        * @return string
+        */
+       protected function getMessageNoIndexFound() {
+
+               $string = $this->language->sL('LLL:EXT:dataquery/locallang_db.xlf:fulltext.no_index');
+               $outputs[] = sprintf($string, $this->table);
+
+               $tables = $this->analyser->getTables();
+               if (!empty($tables)) {
+                       $listOfTables = implode(', ', $tables);
+                       $string = $this->language->sL('LLL:EXT:dataquery/locallang_db.xlf:fulltext.tables_list');
+                       $outputs[] = sprintf($string, $listOfTables);
+               } else {
+                       $outputs = $this->language->sL('LLL:EXT:dataquery/locallang_db.xlf:fulltext.no_table_found');
+               }
+               return implode('<br>', $outputs);
+       }
+}
+
+
+if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['dataquery/Classes/Userfunc/FormEngine.php'])      {
+       include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['dataquery/Classes/Userfunc/FormEngine.php']);
+}
+?>
\ No newline at end of file
diff --git a/Classes/Utility/DatabaseAnalyser.php b/Classes/Utility/DatabaseAnalyser.php
new file mode 100644 (file)
index 0000000..cf3833a
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+/***************************************************************
+*  Copyright notice
+*
+*  (c) 2012-2015 Fabien Udriot (Cobweb) <fudriot@cobweb.ch>
+*  All rights reserved
+*
+*  This script is part of the TYPO3 project. The TYPO3 project is
+*  free software; you can redistribute it and/or modify
+*  it under the terms of the GNU General Public License as published by
+*  the Free Software Foundation; either version 2 of the License, or
+*  (at your option) any later version.
+*
+*  The GNU General Public License can be found at
+*  http://www.gnu.org/copyleft/gpl.html.
+*
+*  This script is distributed in the hope that it will be useful,
+*  but WITHOUT ANY WARRANTY; without even the implied warranty of
+*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*  GNU General Public License for more details.
+*
+*  This copyright notice MUST APPEAR in all copies of the script!
+***************************************************************/
+
+/**
+ * This class provides some API methods related to FULLTEXT indexes
+ *
+ * @author Fabien Udriot (Cobweb) <typo3Tx_Dataquery_DatabaseAnalyser@cobweb.ch>
+ * @author Francois Suter (Cobweb) <typo3Tx_Dataquery_DatabaseAnalyser@cobweb.ch>
+ * @package TYPO3
+ * @subpackage dataquery
+ */
+class Tx_Dataquery_Utility_DatabaseAnalyser {
+       /**
+        * @var t3lib_DB
+        */
+       protected $databaseHandle;
+
+       protected $indices = array();
+
+       /**
+        * Constructor
+        *
+        * @return Tx_Dataquery_Utility_DatabaseAnalyser
+        */
+       public function __construct() {
+               $this->databaseHandle = $GLOBALS['TYPO3_DB'];
+       }
+
+       /**
+        * Returns the list of tables having a FULLTEXT index.
+        *
+        * @return array
+        */
+       public function getTables() {
+               $tables = array();
+               $query = '
+                       SELECT distinct table_name
+                       FROM information_schema.STATISTICS
+                       WHERE index_type = \'FULLTEXT\'
+                               AND table_schema = \'' . TYPO3_db . '\'';
+
+               $resource = $this->databaseHandle->sql_query($query);
+               while ($row = $this->databaseHandle->sql_fetch_assoc($resource)) {
+                       $tables[] = $row['table_name'];
+               }
+               return $tables;
+       }
+
+       /**
+        * Returns the fields composing the FULLTEXT index for the given table.
+        *
+        * @param string $table Name of the table
+        * @return array
+        */
+       public function getFields($table) {
+               // Check if indices were already fetched for given table
+               if (isset($this->indices[$table])) {
+                       return $this->indices[$table];
+               } else {
+                       $indices = array();
+                       $query = 'SELECT index_name, group_concat(distinct column_name) AS fields
+                               FROM information_schema.STATISTICS
+                               WHERE index_type = \'FULLTEXT\'
+                                       AND table_schema = \'' . TYPO3_db . '\'
+                                       AND table_name = \'' . $table . '\'
+                               GROUP BY index_name
+                               ORDER BY index_name';
+
+                       $resource = $this->databaseHandle->sql_query($query);
+
+                       while ($row = $this->databaseHandle->sql_fetch_assoc($resource)) {
+                               if (!empty($row['fields'])) {
+                                       $fields = explode(',',$row['fields']);
+                                       $indices[$row['index_name']] = $table . '.' . implode(',' . $table . '.', $fields);
+                               }
+                       }
+                       $this->indices[$table] = $indices;
+                       return $indices;
+               }
+       }
+
+       /**
+        * Checks whether the given table has a FULLTEXT index or not.
+        *
+        * @param string $table Name of the table
+        * @return boolean
+        */
+       public function hasIndex($table) {
+               $indices = $this->getFields($table);
+               return !empty($indices);
+       }
+
+       /**
+        * Sets the list of tables and fields having fulltext indexing.
+        *
+        * Useful for unit testing.
+        *
+        * @param string $index Name of the index
+        * @param string $table Name of the table
+        * @param array $fields List of tables and fields
+        */
+       public function setIndices($index, $table, $fields) {
+               $this->indices[$table] = array(
+                       $index => $fields
+               );
+       }
+}
+
+if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['dataquery/Classes/Utility/DatabaseAnalyser.php']) {
+       include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['dataquery/Classes/Utility/DatabaseAnalyser.php']);
+}
+?>
\ No newline at end of file
diff --git a/Documentation/Images/FulltextHelpField.png b/Documentation/Images/FulltextHelpField.png
new file mode 100644 (file)
index 0000000..451f2af
Binary files /dev/null and b/Documentation/Images/FulltextHelpField.png differ
diff --git a/Documentation/Queries/Fulltext/Index.rst b/Documentation/Queries/Fulltext/Index.rst
new file mode 100644 (file)
index 0000000..4879b86
--- /dev/null
@@ -0,0 +1,80 @@
+.. ==================================================
+.. FOR YOUR INFORMATION
+.. --------------------------------------------------
+.. -*- coding: utf-8 -*- with BOM.
+
+.. include:: ../../Includes.txt
+
+
+.. _queries-fulltext:
+
+Fulltext searches
+^^^^^^^^^^^^^^^^^
+
+Since version 1.11, Data Query supports fulltext searches. A working
+fulltext query requires several components.
+
+#. First and foremost, the tables which we want to query using
+   a fulltext search must have at least one fulltext index.
+   For example, the "tt_content" table could be extended to be
+   searchable on header and bodytext by using the adding SQL
+   statement to an extension's :file:`ext_tables.sql` file:
+
+   .. code-block:: sql
+
+      CREATE TABLE tt_content (
+          FULLTEXT KEY SEARCH (header,bodytext)
+      );
+
+#. In a Data Query query, the fulltext search index must be used
+   as part of the list of selected fields and reference with an alias.
+   A special syntax is needed, using the :code:`fulltext` keyword and
+   the name of the index (i.e. "SEARCH" from the example above).
+   Example:
+
+   .. code-block:: sql
+      :emphasize-lines: 2
+
+      SELECT uid, pid, header,
+      fulltext:SEARCH AS score
+      FROM tt_content
+
+#. A Data Filter must be defined using either the :code:`fulltext` or
+   the :code:`fulltext_natural` operator. The former will trigger a
+   boolean search, the latter a natural language search. Please refer
+   to the `MySQL documentation for more details <http://dev.mysql.com/doc/refman/5.6/en/fulltext-search.html>`_.
+
+   .. note::
+
+      Query expansion is currently not supported.
+
+   In the Data Filter, the alias declared in the query must be used.
+   In the example below, the first line will lead to a boolean search,
+   the second one to a natural language search:
+
+   .. code-block:: text
+
+      score fulltext gp:sword
+      score fulltext_natural gp_sword
+
+   When the query is interpreted a :code:`MATCH() ... AGAINST` construct
+   is used, automatically placed both in the :code:`SELECT` part and in
+   the :code:`WHERE` clause.
+
+   The alias can of course be used in the "Order by" part of the
+   Data Filter, making it possible to sort the query results by
+   relevance. Example:
+
+   .. code-block:: text
+
+      field = score
+      order = desc
+
+   This will place most relevant results first.
+
+In the TYPO3 BE, Data Query provides some help and hints below the query field.
+
+.. figure:: ../../Images/FulltextHelpField.png
+       :alt: Fulltext hints
+
+       Hints displayed about fulltext indices in the TYPO3 BE
index b0a4e1e..c0b5b0e 100644 (file)
@@ -37,4 +37,5 @@ to writing queries are described in this chapter.
    Comments/Index
    FieldUid/Index
    AdditionalSql/Index
+   Fulltext/Index
 
index 9fe4f29..9ad0651 100644 (file)
@@ -713,13 +713,34 @@ class tx_dataquery_parser {
                                                $tableForApplication = $this->queryObject->mainTable;
                                        }
                                        foreach ($filterData['conditions'] as $conditionData) {
-                                                       // If the value is special value "\all", all values must be taken,
-                                                       // so the condition is simply ignored
+                                               // If the value is special value "\all", all values must be taken,
+                                               // so the condition is simply ignored
                                                if ($conditionData['value'] != '\all') {
-                                                       if (!empty($condition)) {
-                                                               $condition .= ' AND ';
+                                                       try {
+                                                               $parsedCondition = tx_dataquery_SqlUtility::conditionToSql($fullField, $table, $conditionData);
+                                                               if (!empty($condition)) {
+                                                                       $condition .= ' AND ';
+                                                               }
+                                                               $condition .= '(' . $parsedCondition . ')';
+                                                               // If the operator was a full text search, store resulting condition which will be used
+                                                               // later to replace the placeholder in the SELECT part of the statement
+                                                               if ($conditionData['operator'] == 'fulltext' || $conditionData['operator'] == 'fulltext_natural') {
+                                                                       $fullFieldParts = explode('.', $fullField);
+                                                                       $placeholderKey = $table . '.fulltext.' . $fullFieldParts[2];
+                                                                       if (isset($this->queryObject->fulltextSearchPlaceholders[$placeholderKey])) {
+                                                                               $this->queryObject->fulltextSearchPlaceholders[$placeholderKey] = $condition;
+                                                                       }
+                                                               }
+                                                       }
+                                                       catch (tx_tesseract_exception $e) {
+                                                               $this->parentObject->getController()->addMessage(
+                                                                       self::$extKey,
+                                                                       $e->getMessage(),
+                                                                       'Condition ignored',
+                                                                       t3lib_FlashMessage::WARNING,
+                                                                       $filterData
+                                                               );
                                                        }
-                                                       $condition .= '(' . tx_dataquery_SqlUtility::conditionToSql($fullField, $table, $conditionData) . ')';
                                                }
                                        }
                                                // Add the condition only if it wasn't empty
@@ -850,19 +871,25 @@ class tx_dataquery_parser {
        }
 
        /**
-        * This method builds up the query with all the data stored in the structure
+        * Builds up the query with all the data stored in the structure.
         *
-        * @return      string          the assembled SQL query
+        * @return string The assembled SQL query
         */
        public function buildQuery() {
-                       // First check what to do with ORDER BY fields
+               // First check what to do with ORDER BY fields
                $this->preprocessOrderByFields();
-                       // Start assembling the query
+               // Start assembling the query
                $query  = 'SELECT ';
                if ($this->queryObject->structure['DISTINCT']) {
                        $query .= 'DISTINCT ';
                }
                $query .= implode(', ', $this->queryObject->structure['SELECT']) . ' ';
+
+               // Process fulltext replacements, if any
+               foreach ($this->queryObject->fulltextSearchPlaceholders as $placeholder => $replacement) {
+                       $query = str_replace($placeholder, $replacement, $query);
+               }
+
                $query .= 'FROM ' . $this->queryObject->structure['FROM']['table'];
                if (!empty($this->queryObject->structure['FROM']['alias'])) {
                        $query .= ' AS ' . $this->queryObject->structure['FROM']['alias'];
@@ -903,6 +930,7 @@ class tx_dataquery_parser {
                                $query .= ' OFFSET ' . $this->queryObject->structure['OFFSET'];
                        }
                }
+
 //t3lib_utility_Debug::debug($query);
                return $query;
        }
index 48f0037..be52816 100644 (file)
@@ -2,7 +2,7 @@
 /***************************************************************
 *  Copyright notice
 *
-*  (c) 2008-2010 Francois Suter (Cobweb) <typo3@cobweb.ch>
+*  (c) 2008-2015 Francois Suter (Cobweb) <typo3@cobweb.ch>
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
  * @author             Francois Suter (Cobweb) <typo3@cobweb.ch>
  * @package            TYPO3
  * @subpackage tx_dataquery
- *
- * $Id$
  */
 class tx_dataquery_QueryObject {
-               /**
-                * Contains all components of the parsed query
-                * @var array   $structure
-                */
+       /**
+        * Contains all components of the parsed query
+        * @var array $structure
+        */
        public $structure = array();
 
-               /**
-                * Name (or alias if defined) of the main query table, i.e. the first one in the FROM part of the query
-                * @var string  $mainTable
-                */
+       /**
+        * Name (or alias if defined) of the main query table, i.e. the first one in the FROM part of the query
+        * @var string $mainTable
+        */
        public $mainTable;
 
-               /**
-                * List of all subtables, i.e. tables in the JOIN statements
-                * @var array   $subtables
-                */
+       /**
+        * List of all subtables, i.e. tables in the JOIN statements
+        * @var array $subtables
+        */
        public $subtables = array();
 
-               /**
-                * The keys to this array are the aliases of the tables used in the query and they point to the true table names
-                * @var array   $aliases
-                */
+       /**
+        * The keys to this array are the aliases of the tables used in the query and they point to the true table names
+        * @var array $aliases
+        */
        public $aliases = array();
 
-               /**
-                * For each table, record with boolean values whether it has some base fields or not
-                * @var array   $hasBaseFields
-                */
+       /**
+        * For each table, record with boolean values whether it has some base fields or not
+        * @var array $hasBaseFields
+        */
        public $hasBaseFields = array();
 
-               /**
-                * Array with all information of the fields used to order data
-                * @var array   $orderFields
-                */
+       /**
+        * Array with all information of the fields used to order data
+        * @var array $orderFields
+        */
        public $orderFields = array();
 
-               /**
-                * List of aliases for all fields that have one, per table
-                * @var array   $fieldAliases
-                */
+       /**
+        * List of aliases for all fields that have one, per table
+        * @var array $fieldAliases
+        */
        public $fieldAliases = array();
 
-               /**
-                * List of what field aliases map to (table, field and whether it's a function or not)
-                * @var array   $fieldAliasMappings
-                */
+       /**
+        * List of what field aliases map to (table, field and whether it's a function or not)
+        * @var array $fieldAliasMappings
+        */
        public $fieldAliasMappings = array();
 
+       /**
+        * List of placeholders for replacement by MATCH() statements
+        * @var array
+        */
+       public $fulltextSearchPlaceholders = array();
 
        public function __construct() {
                        // Initialize some values
index 3115c47..69e96ac 100644 (file)
@@ -180,8 +180,8 @@ class tx_dataquery_sqlparser {
                                case 'MERGED':
                                        $this->isMergedResult = true;
                                        break;
- *
- */
+*
+*/
                        }
                }
                        // Free some memory
@@ -308,7 +308,7 @@ class tx_dataquery_sqlparser {
                        return;
                }
 
-                       // If the string is just * (or possibly table.*), get all the fields for the table
+               // If the string is just * (or possibly table.*), get all the fields for the table
                if ($hasWildcard) {
                                // It's only *, set table as main table
                        if ($fieldString === '*') {
@@ -343,7 +343,7 @@ class tx_dataquery_sqlparser {
                                );
                        }
 
-                       // Else, the field is some string, analyse it
+               // Else, the field is some string, analyse it
                } else {
 
                                // If there's an alias, extract it and continue parsing
@@ -370,25 +370,37 @@ class tx_dataquery_sqlparser {
                                        $fieldAlias = 'function_' . $this->numFunctions;
                                }
 
-                               // There's no function call
+                       // There's no function call
                        } else {
 
-                                       // If there's a dot, get table name
+                               // If there's a dot, get table name
                                if (stristr($fieldString, '.')) {
                                        $fieldParts = t3lib_div::trimExplode('.', $fieldString, 1);
                                        $table = (isset($this->queryObject->aliases[$fieldParts[0]]) ? $this->queryObject->aliases[$fieldParts[0]] : $fieldParts[0]);
                                        $alias = $fieldParts[0];
                                        $field = $fieldParts[1];
 
-                                       // No dot, the table is the main one
+                               // No dot, the table is the main one
                                } else {
                                        $alias = $this->queryObject->mainTable;
                                        $table = (isset($this->queryObject->aliases[$alias]) ? $this->queryObject->aliases[$alias] : $alias);
                                        $field = $fieldString;
                                }
                        }
-                               // Set the appropriate flag if the field is uid or pid
-                               // Initialize first, if not yet done
+
+                       // For fulltext search, create placeholder which is replaced later with the full MATCH() statement
+                       // (if necessary)
+                       if (strpos($field, 'fulltext:') !== FALSE || strpos($field, 'fulltext_natural:') !== FALSE) {
+                               $fulltextSearchParts = explode(':', $field);
+                               $field = 'fulltext.' . $fulltextSearchParts[1];
+                               // Create placeholder entry (to be filled later)
+                               // If no fulltext value is entered or the table has no fulltext index, the dummy value "1" will be used,
+                               // which is neutral to the query.
+                               $this->queryObject->fulltextSearchPlaceholders[$table . '.' . $field] = '1';
+                       }
+
+                       // Set the appropriate flag if the field is uid or pid
+                       // Initialize first, if not yet done
                        if (!isset($this->queryObject->hasBaseFields[$alias])) {
                                $this->queryObject->hasBaseFields[$alias] = array('uid' => FALSE, 'pid' => FALSE);
                        }
index bd90a4d..c0d50a5 100644 (file)
@@ -2,7 +2,7 @@
 /***************************************************************
 *  Copyright notice
 *
-*  (c) 2012 Francois Suter (Cobweb) <typo3@cobweb.ch>
+*  (c) 2012-2015 Francois Suter (Cobweb) <typo3@cobweb.ch>
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
  * @author             Francois Suter (Cobweb) <typo3@cobweb.ch>
  * @package            TYPO3
  * @subpackage tx_dataquery
- *
- * $Id$
  */
 final class tx_dataquery_SqlUtility {
+       /**
+        * @var Tx_Dataquery_Parser_Fulltext Local instance of full text parser utility
+        */
+       static protected $fulltextParser = NULL;
+
+       /**
+        * Transforms a condition transmitted by data-filter to a real SQL segment.
+        *
+        * @throws tx_tesseract_exception
+        * @param string $field
+        * @param string $table
+        * @param array $conditionData
+        *              + operator: andgroup, orgroup, like, start, fulltext
+        *              + value: the value given as input
+        *              + negate: negate the expression
+        * @return string
+        */
        static public function conditionToSql($field, $table, $conditionData) {
+
                $condition = '';
                        // If the value is special value "\all", all values must be taken,
                        // so the condition is simply ignored
@@ -77,7 +93,7 @@ final class tx_dataquery_SqlUtility {
                                        $condition = 'NOT (' . $condition . ')';
                                }
 
-                               // If the operator is "like", "start" or "end", the SQL operator is always LIKE, but different wildcards are used
+                       // If the operator is "like", "start" or "end", the SQL operator is always LIKE, but different wildcards are used
                        } elseif ($conditionData['operator'] == 'like' || $conditionData['operator'] == 'start' || $conditionData['operator'] == 'end') {
                                        // Make sure values are an array
                                $values = $conditionData['value'];
@@ -104,8 +120,20 @@ final class tx_dataquery_SqlUtility {
                                        $condition = 'NOT (' . $condition . ')';
                                }
 
-                               // Other operators are handled simply
-                               // We just need to take care of special values: "\empty" and "\null"
+                       // Operator "fulltext" requires some special care, as a full MATCH() condition must be assembled
+                       } elseif ($conditionData['operator'] == 'fulltext' || $conditionData['operator'] == 'fulltext_natural') {
+                               $fulltextParser = self::getFulltextParserInstance();
+                               $fulltextParts = explode('.', $field);
+                               $condition = $fulltextParser->parse(
+                                       $table,
+                                       $fulltextParts[2],
+                                       $conditionData['value'],
+                                       ($conditionData['operator'] == 'fulltext_natural'),
+                                       $conditionData['negate']
+                               );
+
+                       // Other operators are handled simply
+                       // We just need to take care of special values: "\empty" and "\null"
                        } else {
                                $operator = $conditionData['operator'];
                                        // Make sure values are an array
@@ -143,5 +171,28 @@ final class tx_dataquery_SqlUtility {
                }
                return $condition;
        }
+
+       /**
+        * Returns an instance of Tx_Dataquery_Parser_Fulltext, which is created on demand.
+        *
+        * @return Tx_Dataquery_Parser_Fulltext
+        */
+       static public function getFulltextParserInstance() {
+               if (self::$fulltextParser === NULL) {
+                       self::$fulltextParser = t3lib_div::makeInstance('Tx_Dataquery_Parser_Fulltext');
+               }
+               return self::$fulltextParser;
+       }
+
+       /**
+        * Sets the fulltext parser instance.
+        *
+        * This is used for unit tests.
+        *
+        * @param Tx_Dataquery_Parser_Fulltext $fulltextParser
+        */
+       static public function setFulltextParserInstance($fulltextParser) {
+               self::$fulltextParser = $fulltextParser;
+       }
 }
 ?>
\ No newline at end of file
index d7ddefa..b261038 100644 (file)
@@ -1,8 +1,6 @@
 <?php
-/* 
+/*
  * Register necessary class names with autoloader
- *
- * $Id$
  */
 $extensionPath = t3lib_extMgm::extPath('dataquery');
 return array(
@@ -13,5 +11,8 @@ return array(
        'tx_dataquery_queryobject' => $extensionPath . 'class.tx_dataquery_queryobject.php',
        'tx_dataquery_sqlparser' => $extensionPath . 'class.tx_dataquery_sqlparser.php',
        'tx_dataquery_wrapper' => $extensionPath . 'class.tx_dataquery_wrapper.php',
+       'tx_dataquery_utility_databaseanalyser'  => $extensionPath . 'Classes/Utility/DatabaseAnalyser.php',
+       'tx_dataquery_parser_fulltext'  => $extensionPath . 'Classes/Parser/Fulltext.php',
+       'tx_dataquery_userfunc_formengine'  => $extensionPath . 'Classes/Userfunc/FormEngine.php',
 );
-?>
+?>
\ No newline at end of file
index 9efeedc..d8991f9 100644 (file)
@@ -2,3 +2,6 @@
 
 # cat=basic/limits/; type=integer; label=Cache limit: Limit size (in bytes) beyond which the cache is not written to preserve the database. Leave empty or set to 0 for no limits.
 cacheLimit = 1000000
+
+# cat=basic/enable/; type=integer; label=Minimum word length: The minimum length of words in the full text index. This must be at least the value of the ft_min_word_len variable in the MySQL server configuration.
+fullTextMinimumWordLength = 4
index a16204c..a1737dc 100644 (file)
@@ -27,10 +27,10 @@ $EM_CONF[$_EXTKEY] = array (
   'clearCacheOnLoad' => 0,
   'lockType' => '',
   'author_company' => '',
-  'version' => '1.10.0',
-  'constraints' => 
+  'version' => '1.11.0-dev',
+  'constraints' =>
   array (
-    'depends' => 
+    'depends' =>
     array (
       'tesseract' => '1.5.0-0.0.0',
       'datafilter' => '1.6.0-0.0.0',
@@ -38,17 +38,17 @@ $EM_CONF[$_EXTKEY] = array (
       'typo3' => '4.5.0-6.2.99',
       'expressions' => '',
     ),
-    'conflicts' => 
+    'conflicts' =>
     array (
     ),
-    'suggests' => 
+    'suggests' =>
     array (
       'devlog' => '',
       'cachecleaner' => '',
     ),
   ),
   '_md5_values_when_last_written' => 'a:36:{s:9:"ChangeLog";s:4:"5681";s:27:"class.tx_dataquery_ajax.php";s:4:"2d32";s:28:"class.tx_dataquery_cache.php";s:4:"1106";s:29:"class.tx_dataquery_parser.php";s:4:"a33b";s:34:"class.tx_dataquery_queryobject.php";s:4:"b13b";s:32:"class.tx_dataquery_sqlparser.php";s:4:"f447";s:33:"class.tx_dataquery_sqlutility.php";s:4:"9e86";s:30:"class.tx_dataquery_wrapper.php";s:4:"fc7e";s:16:"ext_autoload.php";s:4:"6a06";s:21:"ext_conf_template.txt";s:4:"bd30";s:12:"ext_icon.gif";s:4:"ebf0";s:17:"ext_localconf.php";s:4:"c03f";s:14:"ext_tables.php";s:4:"28e0";s:14:"ext_tables.sql";s:4:"5115";s:13:"locallang.xml";s:4:"8d70";s:37:"locallang_csh_txdatafilterfilters.xml";s:4:"73bc";s:36:"locallang_csh_txdataqueryqueries.xml";s:4:"d2d2";s:16:"locallang_db.xml";s:4:"fe86";s:10:"README.txt";s:4:"b3a6";s:7:"tca.php";s:4:"cd0f";s:14:"doc/manual.pdf";s:4:"f3da";s:14:"doc/manual.sxw";s:4:"7548";s:14:"doc/manual.txt";s:4:"f4d7";s:43:"hooks/class.tx_dataquery_datafilterhook.php";s:4:"43e3";s:34:"res/icons/add_dataquery_wizard.gif";s:4:"909a";s:39:"res/icons/icon_tx_dataquery_queries.gif";s:4:"ebf0";s:22:"res/js/check_wizard.js";s:4:"b347";s:42:"samples/class.tx_dataquery_sample_hook.php";s:4:"6530";s:34:"tests/tx_dataquery_parser_Test.php";s:4:"bdb8";s:46:"tests/tx_dataquery_sqlbuilder_default_Test.php";s:4:"8bf8";s:47:"tests/tx_dataquery_sqlbuilder_language_Test.php";s:4:"5b9c";s:38:"tests/tx_dataquery_sqlbuilder_Test.php";s:4:"2255";s:48:"tests/tx_dataquery_sqlbuilder_workspace_Test.php";s:4:"7b25";s:37:"tests/tx_dataquery_sqlparser_Test.php";s:4:"1d40";s:35:"tests/tx_dataquery_wrapper_Test.php";s:4:"c609";s:44:"wizards/class.tx_dataquery_wizards_check.php";s:4:"7a93";}',
-  'suggests' => 
+  'suggests' =>
   array (
   ),
   'comment' => 'Compatibility with TYPO3 CMS 6.2; fixed bug with lost localized uid information.',
index a80bb4d..5f72487 100644 (file)
@@ -1,5 +1,4 @@
 <?xml version="1.0" encoding="utf-8" standalone="yes" ?>
-<!-- $Id$-->
 <T3locallang>
        <meta type="array">
                <type>database</type>
@@ -12,6 +11,7 @@
                        <label index="tx_dataquery_queries.title">Title</label>
                        <label index="tx_dataquery_queries.description">Description</label>
                        <label index="tx_dataquery_queries.sql_query">SQL Query</label>
+                       <label index="tx_dataquery_queries.fulltext_indices">Full Text Indices</label>
                        <label index="tx_dataquery_queries.cache_duration">Cache duration (in seconds)</label>
                        <label index="tx_dataquery_queries.ignore_enable_fields">Ignore enable fields</label>
                        <label index="tx_dataquery_queries.ignore_enable_fields.I.0">Don't ignore any fields</label>
                        <label index="wizards.add_dataquery">Add a new Data Query</label>
                        <label index="tx_datafilter_filters.tx_dataquery_sql">Additional SQL WHERE clause</label>
                        <label index="sql">SQL</label>
+                       <label index="fulltext.no_query">To check for FULLTEXT indices, please write a query first and save it at least once.</label>
+                       <label index="fulltext.table_name">Table (with FULLTEXT index)</label>
+                       <label index="fulltext.fulltext_fields">Fields composing the FULLTEXT index</label>
+                       <label index="fulltext.no_index_or_missing_table">No FULLTEXT index found for this table or incomplete SQL query.</label>
+                       <label index="fulltext.no_index">No FULLTEXT index for table "%s".</label>
+                       <label index="fulltext.tables_list">FULLTEXT index found in table(s): %s</label>
+                       <label index="fulltext.no_table_found">Actually, I couldn't find any tables having a FULLTEXT index.</label>
+                       <label index="fulltext.indexed_fields">where "%s" is an index composed by fields: %s</label>
+                       <label index="fulltext.syntax_for_query">Marker to be placed in the SELECT part:</label>
                </languageKey>
                <languageKey index="fr" type="array">
                        <label index="tx_dataquery_queries">Requête</label>
diff --git a/tca.php b/tca.php
index 2800ae3..607979e 100644 (file)
--- a/tca.php
+++ b/tca.php
@@ -58,6 +58,14 @@ $GLOBALS['TCA']['tx_dataquery_queries'] = array(
                                )
                        )
                ),
+               'fulltext_indices' => array(
+                       'exclude' => 0,
+                       'label' => 'LLL:EXT:dataquery/locallang_db.xlf:tx_dataquery_queries.fulltext_indices',
+                       'config' => array(
+                               'type' => 'user',
+                               'userFunc' => 'Tx_Dataquery_Userfunc_FormEngine->renderFulltextIndices',
+                       )
+               ),
                'cache_duration' => array(
                        'exclude' => 1,
                        'label' => 'LLL:EXT:dataquery/locallang_db.xml:tx_dataquery_queries.cache_duration',
@@ -137,7 +145,7 @@ $GLOBALS['TCA']['tx_dataquery_queries'] = array(
                ),
        ),
        'types' => array(
-               '0' => array('showitem' => 'hidden;;;;1-1-1, title;;1;;2-2-2, sql_query;;;;3-3-3,
+               '0' => array('showitem' => 'hidden;;;;1-1-1, title;;1;;2-2-2, sql_query;;;;3-3-3, fulltext_indices,
                                                                        --div--;LLL:EXT:dataquery/locallang_db.xml:tx_dataquery_queries.tab.advanced, cache_duration;;;;1-1-1, ignore_enable_fields;;2;;2-2-2 , ignore_language_handling;;3;;3-3-3, get_versions_directly')
        ),
        'palettes' => array(
index 2795c0d..1314f38 100644 (file)
@@ -636,7 +636,7 @@ abstract class tx_dataquery_sqlbuilder_Test extends tx_phpunit_testcase {
                $generalCondition = $this->finalizeCondition($this->fullConditionForTable);
                $additionalSelectFields = $this->prepareAdditionalFields('tt_content');
                $expectedResult = 'SELECT tt_content.uid, tt_content.header, FROM_UNIXTIME(tstamp, \'%Y\') AS year, tt_content.pid, tt_content.sys_language_uid' . $additionalSelectFields . ' FROM tt_content AS tt_content WHERE ' . $generalCondition;
-                       // Add the filter's condition if not empty
+               // Add the filter's condition if not empty
                if (!empty($condition)) {
                        if ($isSqlCondition) {
                                $expectedResult .= 'AND (' . $condition . ') ';
@@ -656,6 +656,230 @@ abstract class tx_dataquery_sqlbuilder_Test extends tx_phpunit_testcase {
        }
 
        /**
+        * Provides filters for testing query with filters.
+        *
+        * Some filters are arbitrarily negated, to test the building of negated conditions
+        * Also provides the expected interpretation of the filter.
+        *
+        * @return array
+        */
+       public function fulltextFilterProvider() {
+               $filters = array(
+                       // Boolean mode, one word valid, one word ignore
+                       'fulltext, one valid word, one invalid word' => array(
+                               'filter' => array(
+                                       'filters' => array(
+                                               0 => array(
+                                                       'table' => 'tt_content',
+                                                       'field' => 'score',
+                                                       'conditions' => array(
+                                                               0 => array(
+                                                                       'operator' => 'fulltext',
+                                                                       // "bar" should be ignored, as it is below minimum word length
+                                                                       'value' => 'foox bar',
+                                                                       'negate' => FALSE
+                                                               )
+                                                       )
+                                               )
+                                       )
+                               ),
+                               'index' => 'SEARCH',
+                               'fulltextCondition' => '(MATCH(tt_content.header,tt_content.bodytext) AGAINST(\'+"foox"\' IN BOOLEAN MODE))'
+                       ),
+                       // Boolean mode, one word included, one word excluded
+                       'fulltext, one word included, one word excluded' => array(
+                               'filter' => array(
+                                       'filters' => array(
+                                               0 => array(
+                                                       'table' => 'tt_content',
+                                                       'field' => 'score',
+                                                       'conditions' => array(
+                                                               0 => array(
+                                                                       'operator' => 'fulltext',
+                                                                       'value' => 'foox -barz',
+                                                                       'negate' => FALSE
+                                                               )
+                                                       )
+                                               )
+                                       )
+                               ),
+                               'index' => 'SEARCH',
+                               'fulltextCondition' => '(MATCH(tt_content.header,tt_content.bodytext) AGAINST(\'+"foox" -"barz"\' IN BOOLEAN MODE))'
+                       ),
+                       // Boolean mode with quoted string
+                       'fulltext, quoted string' => array(
+                               'filter' => array(
+                                       'filters' => array(
+                                               0 => array(
+                                                       'table' => 'tt_content',
+                                                       'field' => 'score',
+                                                       'conditions' => array(
+                                                               0 => array(
+                                                                       'operator' => 'fulltext',
+                                                                       'value' => '"go for foox"',
+                                                                       'negate' => FALSE
+                                                               )
+                                                       )
+                                               )
+                                       )
+                               ),
+                               'index' => 'SEARCH',
+                               'fulltextCondition' => '(MATCH(tt_content.header,tt_content.bodytext) AGAINST(\'+"go for foox"\' IN BOOLEAN MODE))'
+                       ),
+                       // Boolean mode, negated condition
+                       'fulltext, negated condition' => array(
+                               'filter' => array(
+                                       'filters' => array(
+                                               0 => array(
+                                                       'table' => 'tt_content',
+                                                       'field' => 'score',
+                                                       'conditions' => array(
+                                                               0 => array(
+                                                                       'operator' => 'fulltext',
+                                                                       // "bar" should be ignored, as it is below minimum word length
+                                                                       'value' => 'foox',
+                                                                       'negate' => TRUE
+                                                               )
+                                                       )
+                                               )
+                                       )
+                               ),
+                               'index' => 'SEARCH',
+                               'fulltextCondition' => '(NOT MATCH(tt_content.header,tt_content.bodytext) AGAINST(\'+"foox"\' IN BOOLEAN MODE))'
+                       ),
+                       // Natural mode
+                       'fulltext natural' => array(
+                               'filter' => array(
+                                       'filters' => array(
+                                               0 => array(
+                                                       'table' => 'tt_content',
+                                                       'field' => 'score',
+                                                       'conditions' => array(
+                                                               0 => array(
+                                                                       'operator' => 'fulltext_natural',
+                                                                       'value' => 'foo bar',
+                                                                       'negate' => FALSE
+                                                               )
+                                                       )
+                                               )
+                                       )
+                               ),
+                               'index' => 'SEARCH',
+                               'fulltextCondition' => '(MATCH(tt_content.header,tt_content.bodytext) AGAINST(\'foo bar\'))'
+                       ),
+                       // Empty search words
+                       'fulltext, empty search' => array(
+                               'filter' => array(
+                                       'filters' => array(
+                                               0 => array(
+                                                       'table' => 'tt_content',
+                                                       'field' => 'score',
+                                                       'conditions' => array(
+                                                               0 => array(
+                                                                       'operator' => 'fulltext',
+                                                                       'value' => '',
+                                                                       'negate' => FALSE
+                                                               )
+                                                       )
+                                               )
+                                       )
+                               ),
+                               'index' => 'SEARCH',
+                               'fulltextCondition' => '1'
+                       ),
+                       // Empty search words in natural mode
+                       'fulltext natural, empty search' => array(
+                               'filter' => array(
+                                       'filters' => array(
+                                               0 => array(
+                                                       'table' => 'tt_content',
+                                                       'field' => 'score',
+                                                       'conditions' => array(
+                                                               0 => array(
+                                                                       'operator' => 'fulltext_natural',
+                                                                       'value' => '',
+                                                                       'negate' => FALSE
+                                                               )
+                                                       )
+                                               )
+                                       )
+                               ),
+                               'index' => 'SEARCH',
+                               'fulltextCondition' => '1'
+                       ),
+                       // Invalid index
+                       'fulltext, invalid index' => array(
+                               'filter' => array(
+                                       'filters' => array(
+                                               0 => array(
+                                                       'table' => 'tt_content',
+                                                       'field' => 'score',
+                                                       'conditions' => array(
+                                                               0 => array(
+                                                                       'operator' => 'fulltext',
+                                                                       // "bar" should be ignored, as it is below minimum word length
+                                                                       'value' => 'foox bar',
+                                                                       'negate' => FALSE
+                                                               )
+                                                       )
+                                               )
+                                       )
+                               ),
+                               'index' => 'WEIRD',
+                               'fulltextCondition' => '1'
+                       ),
+               );
+               return $filters;
+       }
+
+       /**
+        * Parses and rebuilds a SELECT query with a filter.
+        *
+        * @param array $filter Filter configuration
+        * @param string $index Name of the fulltext index
+        * @param string $fulltextCondition Interpreted condition
+        * @test
+        * @dataProvider fulltextFilterProvider
+        */
+       public function selectQueryWithFulltextFilter($filter, $index, $fulltextCondition) {
+               /** @var Tx_Dataquery_Parser_Fulltext $fulltextParser */
+               $fulltextParser = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('Tx_Dataquery_Parser_Fulltext');
+               /** @var Tx_Dataquery_Utility_DatabaseAnalyser $databaseAnalyser */
+               $databaseAnalyser = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('Tx_Dataquery_Utility_DatabaseAnalyser');
+               // Set "fake" fulltext indices to ensure proper running of unit test
+               $databaseAnalyser->setIndices(
+                       'SEARCH',
+                       'tt_content',
+                       'tt_content.header,tt_content.bodytext'
+               );
+               $fulltextParser->setAnalyser($databaseAnalyser);
+               // Set "fake" configuration extension to ensure proper running of unit test
+               $fulltextParser->setConfiguration(
+                       array(
+                               'fullTextMinimumWordLength' => 4
+                       )
+               );
+               tx_dataquery_SqlUtility::setFulltextParserInstance($fulltextParser);
+               // Replace markers in the condition
+               $generalCondition = $this->finalizeCondition($this->fullConditionForTable);
+               $additionalSelectFields = $this->prepareAdditionalFields('tt_content');
+               $expectedResult = 'SELECT tt_content.uid, tt_content.header, ' . $fulltextCondition . ' AS score, tt_content.pid, tt_content.sys_language_uid' . $additionalSelectFields . ' FROM tt_content AS tt_content WHERE ';
+               $expectedResult .= $generalCondition;
+               if ($fulltextCondition !== '1') {
+                       $expectedResult .= 'AND ((' . $fulltextCondition . ')) ';
+               }
+
+               $query = 'SELECT uid,header,fulltext:' . $index . ' AS score FROM tt_content';
+               $this->sqlParser->parseQuery($query);
+               $this->sqlParser->setProviderData($this->settings);
+               $this->sqlParser->addTypo3Mechanisms();
+               $this->sqlParser->addFilter($filter);
+               $actualResult = $this->sqlParser->buildQuery();
+
+               $this->assertEquals($expectedResult, $actualResult, '***Expected***' . $expectedResult . '***Actual***' . $actualResult);
+       }
+
+       /**
         * Provides various setups for all ignore flags.
         *
         * Also provides the corresponding expected WHERE clauses.