From a4ec309d2df474f807b5cb44e7f01740325248d3 Mon Sep 17 00:00:00 2001
From: Christian Kuhn <lolli@schwarzbu.ch>
Date: Tue, 6 Jun 2017 17:57:15 +0200
Subject: [PATCH] [BUGFIX] mssql: ExpressionBuilder inSet() support

mssql does not support FIND_IN_SET(). The patch adds a
solution based on LIKE.
Since the query fiddling in this area is a bit tricky,
this area is now supported by a bunch of functional tests.
A postgres bug those new functional tests reveal is fixed
along the way.

Change-Id: I5e94ad8df7a37a680b457eff1b5b16a0c14dba39
Resolves: #81488
Releases: master, 8.7
Reviewed-on: https://review.typo3.org/53141
Reviewed-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Tested-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Markus Klein <markus.klein@typo3.org>
Tested-by: Markus Klein <markus.klein@typo3.org>
---
 .../Query/Expression/ExpressionBuilder.php    |  32 +-
 .../DataSet/TestExpressionBuilderInSet.csv    |  42 ++
 .../test_expressionbuilder/ext_emconf.php     |  22 +
 .../test_expressionbuilder/ext_tables.sql     |  13 +
 .../Expression/ExpressionBuilderTest.php      | 399 ++++++++++++++++++
 .../Expression/ExpressionBuilderTest.php      |  46 ++
 .../Generic/Storage/Typo3DbQueryParser.php    |   2 +-
 7 files changed, 548 insertions(+), 8 deletions(-)
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/Fixtures/DataSet/TestExpressionBuilderInSet.csv
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/Fixtures/Extensions/test_expressionbuilder/ext_emconf.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/Fixtures/Extensions/test_expressionbuilder/ext_tables.sql
 create mode 100644 typo3/sysext/core/Tests/Functional/Database/Query/Expression/ExpressionBuilderTest.php

diff --git a/typo3/sysext/core/Classes/Database/Query/Expression/ExpressionBuilder.php b/typo3/sysext/core/Classes/Database/Query/Expression/ExpressionBuilder.php
index ba8335925df1..f0fd694d4718 100644
--- a/typo3/sysext/core/Classes/Database/Query/Expression/ExpressionBuilder.php
+++ b/typo3/sysext/core/Classes/Database/Query/Expression/ExpressionBuilder.php
@@ -270,7 +270,7 @@ class ExpressionBuilder
     /**
      * Returns a comparison that can find a value in a list field (CSV).
      *
-     * @param string $fieldName The fieldname. Will be quoted according to database platform automatically.
+     * @param string $fieldName The field name. Will be quoted according to database platform automatically.
      * @param string $value Argument to be used in FIND_IN_SET() comparison. No automatic quoting/escaping is done.
      * @param bool $isColumn Set when the value to compare is a column on a table to activate casting
      * @return string
@@ -297,7 +297,7 @@ class ExpressionBuilder
             case 'postgresql':
             case 'pdo_postgresql':
                 return $this->comparison(
-                    $isColumn ? $value . '::text' : $value,
+                    $isColumn ? $value . '::text' : $this->literal($this->unquoteLiteral((string)$value)),
                     self::EQ,
                     sprintf(
                         'ANY(string_to_array(%s, %s))',
@@ -315,11 +315,29 @@ class ExpressionBuilder
                 break;
             case 'sqlsrv':
             case 'pdo_sqlsrv':
-                throw new \RuntimeException(
-                    'FIND_IN_SET support for database platform "SQLServer" not yet implemented.',
-                    1459696681
-                );
-                break;
+            case 'mssql':
+                // See unit and functional tests for details
+                if ($isColumn) {
+                    $expression = $this->orX(
+                        $this->eq($fieldName, $value),
+                        $this->like($fieldName, $value . ' + \',%\''),
+                        $this->like($fieldName, '\'%,\' + ' . $value),
+                        $this->like($fieldName, '\'%,\' + ' . $value . ' + \',%\'')
+                    );
+                } else {
+                    $likeEscapedValue = str_replace(
+                        ['[', '%'],
+                        ['[[]', '[%]'],
+                        $this->unquoteLiteral($value)
+                    );
+                    $expression = $this->orX(
+                        $this->eq($fieldName, $this->literal($this->unquoteLiteral((string)$value))),
+                        $this->like($fieldName, $this->literal($likeEscapedValue . ',%')),
+                        $this->like($fieldName, $this->literal('%,' . $likeEscapedValue)),
+                        $this->like($fieldName, $this->literal('%,' . $likeEscapedValue . ',%'))
+                    );
+                }
+                return (string)$expression;
             case 'sqlite':
             case 'sqlite3':
             case 'pdo_sqlite':
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/DataSet/TestExpressionBuilderInSet.csv b/typo3/sysext/core/Tests/Functional/Database/Fixtures/DataSet/TestExpressionBuilderInSet.csv
new file mode 100644
index 000000000000..26f44dc3dc5f
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/Fixtures/DataSet/TestExpressionBuilderInSet.csv
@@ -0,0 +1,42 @@
+"tx_expressionbuildertest"
+,"uid","pid","aField","aCsvField"
+,1,0,"match","match"
+,2,0,"match","match,nomatch"
+,3,0,"match","nomatch,match"
+,4,0,"match","nomatch1,match,nomatch2"
+,5,0,"match","nomatch"
+,6,0,"2","2"
+,7,0,"2","2,3"
+,8,0,"2","1,2"
+,9,0,"2","1,2,3"
+,10,0,"2","4"
+,11,0,"wild%card","wild%card"
+,12,0,"wild%card","wild%card,nowild%card"
+,13,0,"wild%card","nowild%card,wild%card"
+,14,0,"wild%card","nowild%card1,wild%card,nowild%card2"
+,15,0,"wild%card","nowild%card"
+,16,0,"kokolores","wild[card"
+,17,0,"kokolores","wild[card,nowild[card"
+,18,0,"kokolores","nowild[card,wild[card"
+,19,0,"kokolores","nowild[card1,wild[card,nowild[card2"
+,20,0,"kokolores","nowild[card"
+,21,0,"kokolores","wild]card"
+,22,0,"kokolores","wild]card,nowild]card"
+,23,0,"kokolores","nowild]card,wild]card"
+,24,0,"kokolores","nowild]card1,wild]card,nowild]card2"
+,25,0,"kokolores","nowild]card"
+,26,0,"kokolores","wild[]card"
+,27,0,"kokolores","wild[]card,nowild[]card"
+,28,0,"kokolores","nowild[]card,wild[]card"
+,29,0,"kokolores","nowild[]card1,wild[]card,nowild[]card2"
+,30,0,"kokolores","nowild[]card"
+,31,0,"kokolores","wild[foo]card"
+,32,0,"kokolores","wild[foo]card,nowild[foo]card"
+,33,0,"kokolores","nowild[foo]card,wild[foo]card"
+,34,0,"kokolores","nowild[foo]card1,wild[foo]card,nowild[foo]card2"
+,35,0,"kokolores","nowild[foo]card"
+,36,0,"kokolores","wild[%]card"
+,37,0,"kokolores","wild[%]card,nowild[%]card"
+,38,0,"kokolores","nowild[%]card,wild[%]card"
+,39,0,"kokolores","nowild[%]card1,wild[%]card,nowild[%]card2"
+,40,0,"kokolores","nowild[%]card"
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/Extensions/test_expressionbuilder/ext_emconf.php b/typo3/sysext/core/Tests/Functional/Database/Fixtures/Extensions/test_expressionbuilder/ext_emconf.php
new file mode 100644
index 000000000000..efbc737910de
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/Fixtures/Extensions/test_expressionbuilder/ext_emconf.php
@@ -0,0 +1,22 @@
+<?php
+$EM_CONF[$_EXTKEY] = [
+    'title' => 'ExpressionBuilder Test',
+    'description' => 'ExpressionBuilder Test',
+    'category' => 'example',
+    'version' => '0.0.1',
+    'state' => 'beta',
+    'uploadfolder' => 0,
+    'createDirs' => '',
+    'clearcacheonload' => 0,
+    'author' => 'Christian Kuhn',
+    'author_email' => 'lolli@schwarzu.ch',
+    'constraints' => [
+        'depends' => [
+            'typo3' => '8.7.0-0.0.0',
+        ],
+        'conflicts' => [
+        ],
+        'suggests' => [
+        ],
+    ],
+];
diff --git a/typo3/sysext/core/Tests/Functional/Database/Fixtures/Extensions/test_expressionbuilder/ext_tables.sql b/typo3/sysext/core/Tests/Functional/Database/Fixtures/Extensions/test_expressionbuilder/ext_tables.sql
new file mode 100644
index 000000000000..73b94ca38a1a
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/Fixtures/Extensions/test_expressionbuilder/ext_tables.sql
@@ -0,0 +1,13 @@
+#
+# Table structure for table 'tx_expressionbuildertest'
+#
+CREATE TABLE tx_expressionbuildertest (
+  uid int(11) NOT NULL auto_increment,
+  pid int(11) DEFAULT '0' NOT NULL,
+
+  aField text,
+  aCsvField text,
+
+  PRIMARY KEY (uid),
+  KEY parent (pid)
+);
diff --git a/typo3/sysext/core/Tests/Functional/Database/Query/Expression/ExpressionBuilderTest.php b/typo3/sysext/core/Tests/Functional/Database/Query/Expression/ExpressionBuilderTest.php
new file mode 100644
index 000000000000..92579879b8de
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Database/Query/Expression/ExpressionBuilderTest.php
@@ -0,0 +1,399 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Core\Tests\Functional\Database\Query\Expression;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+/**
+ * Test case
+ */
+class ExpressionBuilderTest extends FunctionalTestCase
+{
+    /**
+     * @var array Extension comes with table setup to test inSet() methods of ExpressionBuilder
+     */
+    protected $testExtensionsToLoad = [
+        'typo3/sysext/core/Tests/Functional/Database/Fixtures/Extensions/test_expressionbuilder',
+    ];
+
+    /**
+     * @test
+     */
+    public function inSetReturnsExpectedDataSetsWithColumn()
+    {
+        $this->importCSVDataSet(__DIR__ . '/../../Fixtures/DataSet/TestExpressionBuilderInSet.csv');
+        $queryBuilder = (new ConnectionPool())->getQueryBuilderForTable('tx_expressionbuildertest');
+        $result = $queryBuilder
+            ->select('uid', 'aCsvField')
+            ->from('tx_expressionbuildertest')
+            ->where(
+                $queryBuilder->expr()->inSet('aCsvField', $queryBuilder->quoteIdentifier('aField'), true)
+            )
+            ->orderBy('uid')
+            ->execute()
+            ->fetchAll();
+        $expected = [
+            0 => [
+                'uid' => 1,
+                'aCsvField' => 'match',
+            ],
+            1 => [
+                'uid' => 2,
+                'aCsvField' => 'match,nomatch',
+            ],
+            2 => [
+                'uid' => 3,
+                'aCsvField' => 'nomatch,match',
+            ],
+            3 => [
+                'uid' => 4,
+                'aCsvField' => 'nomatch1,match,nomatch2',
+            ],
+            // uid 5 missing here!
+            4 => [
+                'uid' => 6,
+                'aCsvField' => '2',
+            ],
+            5 => [
+                'uid' => 7,
+                'aCsvField' => '2,3',
+            ],
+            6 => [
+                'uid' => 8,
+                'aCsvField' => '1,2',
+            ],
+            7 => [
+                'uid' => 9,
+                'aCsvField' => '1,2,3',
+            ],
+            // uid 10 missing here!
+            8 => [
+                'uid' => 11,
+                'aCsvField' => 'wild%card',
+            ],
+            9 => [
+                'uid' => 12,
+                'aCsvField' => 'wild%card,nowild%card',
+            ],
+            10 => [
+                'uid' => 13,
+                'aCsvField' => 'nowild%card,wild%card',
+            ],
+            11 => [
+                'uid' => 14,
+                'aCsvField' => 'nowild%card1,wild%card,nowild%card2',
+            ],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function inSetReturnsExpectedDataSets()
+    {
+        $this->importCSVDataSet(__DIR__ . '/../../Fixtures/DataSet/TestExpressionBuilderInSet.csv');
+        $queryBuilder = (new ConnectionPool())->getQueryBuilderForTable('tx_expressionbuildertest');
+        $result = $queryBuilder
+            ->select('uid', 'aCsvField')
+            ->from('tx_expressionbuildertest')
+            ->where(
+                $queryBuilder->expr()->inSet('aCsvField', $queryBuilder->expr()->literal('match'))
+            )
+            ->orderBy('uid')
+            ->execute()
+            ->fetchAll();
+        $expected = [
+            0 => [
+                'uid' => 1,
+                'aCsvField' => 'match',
+            ],
+            1 => [
+                'uid' => 2,
+                'aCsvField' => 'match,nomatch',
+            ],
+            2 => [
+                'uid' => 3,
+                'aCsvField' => 'nomatch,match',
+            ],
+            3 => [
+                'uid' => 4,
+                'aCsvField' => 'nomatch1,match,nomatch2',
+            ],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function inSetReturnsExpectedDataSetsWithInts()
+    {
+        $this->importCSVDataSet(__DIR__ . '/../../Fixtures/DataSet/TestExpressionBuilderInSet.csv');
+        $queryBuilder = (new ConnectionPool())->getQueryBuilderForTable('tx_expressionbuildertest');
+        $result = $queryBuilder
+            ->select('uid', 'aCsvField')
+            ->from('tx_expressionbuildertest')
+            ->where(
+                $queryBuilder->expr()->inSet('aCsvField', (string)2)
+            )
+            ->orderBy('uid')
+            ->execute()
+            ->fetchAll();
+        $expected = [
+            0 => [
+                'uid' => 6,
+                'aCsvField' => '2',
+            ],
+            1 => [
+                'uid' => 7,
+                'aCsvField' => '2,3',
+            ],
+            2 => [
+                'uid' => 8,
+                'aCsvField' => '1,2',
+            ],
+            3 => [
+                'uid' => 9,
+                'aCsvField' => '1,2,3',
+            ],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function inSetReturnsExpectedDataSetsIfValueContainsLikeWildcard()
+    {
+        $this->importCSVDataSet(__DIR__ . '/../../Fixtures/DataSet/TestExpressionBuilderInSet.csv');
+        $queryBuilder = (new ConnectionPool())->getQueryBuilderForTable('tx_expressionbuildertest');
+        $result = $queryBuilder
+            ->select('uid', 'aCsvField')
+            ->from('tx_expressionbuildertest')
+            ->where(
+                $queryBuilder->expr()->inSet('aCsvField', $queryBuilder->expr()->literal('wild%card'))
+            )
+            ->orderBy('uid')
+            ->execute()
+            ->fetchAll();
+        $expected = [
+            0 => [
+                'uid' => 11,
+                'aCsvField' => 'wild%card',
+            ],
+            1 => [
+                'uid' => 12,
+                'aCsvField' => 'wild%card,nowild%card',
+            ],
+            2 => [
+                'uid' => 13,
+                'aCsvField' => 'nowild%card,wild%card',
+            ],
+            3 => [
+                'uid' => 14,
+                'aCsvField' => 'nowild%card1,wild%card,nowild%card2',
+            ],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function inSetReturnsExpectedDataSetsIfValueContainsBracket()
+    {
+        $this->importCSVDataSet(__DIR__ . '/../../Fixtures/DataSet/TestExpressionBuilderInSet.csv');
+        $queryBuilder = (new ConnectionPool())->getQueryBuilderForTable('tx_expressionbuildertest');
+        $result = $queryBuilder
+            ->select('uid', 'aCsvField')
+            ->from('tx_expressionbuildertest')
+            ->where(
+                $queryBuilder->expr()->inSet('aCsvField', $queryBuilder->expr()->literal('wild[card'))
+            )
+            ->orderBy('uid')
+            ->execute()
+            ->fetchAll();
+        $expected = [
+            0 => [
+                'uid' => 16,
+                'aCsvField' => 'wild[card',
+            ],
+            1 => [
+                'uid' => 17,
+                'aCsvField' => 'wild[card,nowild[card',
+            ],
+            2 => [
+                'uid' => 18,
+                'aCsvField' => 'nowild[card,wild[card',
+            ],
+            3 => [
+                'uid' => 19,
+                'aCsvField' => 'nowild[card1,wild[card,nowild[card2',
+            ],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function inSetReturnsExpectedDataSetsIfValueContainsClosingBracket()
+    {
+        $this->importCSVDataSet(__DIR__ . '/../../Fixtures/DataSet/TestExpressionBuilderInSet.csv');
+        $queryBuilder = (new ConnectionPool())->getQueryBuilderForTable('tx_expressionbuildertest');
+        $result = $queryBuilder
+            ->select('uid', 'aCsvField')
+            ->from('tx_expressionbuildertest')
+            ->where(
+                $queryBuilder->expr()->inSet('aCsvField', $queryBuilder->expr()->literal('wild]card'))
+            )
+            ->orderBy('uid')
+            ->execute()
+            ->fetchAll();
+        $expected = [
+            0 => [
+                'uid' => 21,
+                'aCsvField' => 'wild]card',
+            ],
+            1 => [
+                'uid' => 22,
+                'aCsvField' => 'wild]card,nowild]card',
+            ],
+            2 => [
+                'uid' => 23,
+                'aCsvField' => 'nowild]card,wild]card',
+            ],
+            3 => [
+                'uid' => 24,
+                'aCsvField' => 'nowild]card1,wild]card,nowild]card2',
+            ],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function inSetReturnsExpectedDataSetsIfValueContainsOpeningAndClosingBracket()
+    {
+        $this->importCSVDataSet(__DIR__ . '/../../Fixtures/DataSet/TestExpressionBuilderInSet.csv');
+        $queryBuilder = (new ConnectionPool())->getQueryBuilderForTable('tx_expressionbuildertest');
+        $result = $queryBuilder
+            ->select('uid', 'aCsvField')
+            ->from('tx_expressionbuildertest')
+            ->where(
+                $queryBuilder->expr()->inSet('aCsvField', $queryBuilder->expr()->literal('wild[]card'))
+            )
+            ->orderBy('uid')
+            ->execute()
+            ->fetchAll();
+        $expected = [
+            0 => [
+                'uid' => 26,
+                'aCsvField' => 'wild[]card',
+            ],
+            1 => [
+                'uid' => 27,
+                'aCsvField' => 'wild[]card,nowild[]card',
+            ],
+            2 => [
+                'uid' => 28,
+                'aCsvField' => 'nowild[]card,wild[]card',
+            ],
+            3 => [
+                'uid' => 29,
+                'aCsvField' => 'nowild[]card1,wild[]card,nowild[]card2',
+            ],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function inSetReturnsExpectedDataSetsIfValueContainsBracketsAroundWord()
+    {
+        $this->importCSVDataSet(__DIR__ . '/../../Fixtures/DataSet/TestExpressionBuilderInSet.csv');
+        $queryBuilder = (new ConnectionPool())->getQueryBuilderForTable('tx_expressionbuildertest');
+        $result = $queryBuilder
+            ->select('uid', 'aCsvField')
+            ->from('tx_expressionbuildertest')
+            ->where(
+                $queryBuilder->expr()->inSet('aCsvField', $queryBuilder->expr()->literal('wild[foo]card'))
+            )
+            ->orderBy('uid')
+            ->execute()
+            ->fetchAll();
+        $expected = [
+            0 => [
+                'uid' => 31,
+                'aCsvField' => 'wild[foo]card',
+            ],
+            1 => [
+                'uid' => 32,
+                'aCsvField' => 'wild[foo]card,nowild[foo]card',
+            ],
+            2 => [
+                'uid' => 33,
+                'aCsvField' => 'nowild[foo]card,wild[foo]card',
+            ],
+            3 => [
+                'uid' => 34,
+                'aCsvField' => 'nowild[foo]card1,wild[foo]card,nowild[foo]card2',
+            ],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+
+    /**
+     * @test
+     */
+    public function inSetReturnsExpectedDataSetsIfValueContainsBracketsAroundLikeWildcard()
+    {
+        $this->importCSVDataSet(__DIR__ . '/../../Fixtures/DataSet/TestExpressionBuilderInSet.csv');
+        $queryBuilder = (new ConnectionPool())->getQueryBuilderForTable('tx_expressionbuildertest');
+        $result = $queryBuilder
+            ->select('uid', 'aCsvField')
+            ->from('tx_expressionbuildertest')
+            ->where(
+                $queryBuilder->expr()->inSet('aCsvField', $queryBuilder->expr()->literal('wild[%]card'))
+            )
+            ->orderBy('uid')
+            ->execute()
+            ->fetchAll();
+        $expected = [
+            0 => [
+                'uid' => 36,
+                'aCsvField' => 'wild[%]card',
+            ],
+            1 => [
+                'uid' => 37,
+                'aCsvField' => 'wild[%]card,nowild[%]card',
+            ],
+            2 => [
+                'uid' => 38,
+                'aCsvField' => 'nowild[%]card,wild[%]card',
+            ],
+            3 => [
+                'uid' => 39,
+                'aCsvField' => 'nowild[%]card1,wild[%]card,nowild[%]card2',
+            ],
+        ];
+        $this->assertEquals($expected, $result);
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Database/Query/Expression/ExpressionBuilderTest.php b/typo3/sysext/core/Tests/Unit/Database/Query/Expression/ExpressionBuilderTest.php
index 11961722b62c..57bc5d98c109 100644
--- a/typo3/sysext/core/Tests/Unit/Database/Query/Expression/ExpressionBuilderTest.php
+++ b/typo3/sysext/core/Tests/Unit/Database/Query/Expression/ExpressionBuilderTest.php
@@ -230,6 +230,26 @@ class ExpressionBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCa
         $this->assertSame('aField NOT IN (1, 2, 3)', $result);
     }
 
+    /**
+     * @test
+     */
+    public function inSetThrowsExceptionWithEmptyValue()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1459696089);
+        $this->subject->inSet('aField', '');
+    }
+
+    /**
+     * @test
+     */
+    public function inSetThrowsExceptionWithInvalidValue()
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionCode(1459696090);
+        $this->subject->inSet('aField', 'an,Invalid,Value');
+    }
+
     /**
      * @test
      */
@@ -256,8 +276,10 @@ class ExpressionBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCa
     {
         $databasePlatform = $this->prophesize(MockPlatform::class);
         $databasePlatform->getName()->willReturn('postgresql');
+        $databasePlatform->getStringLiteralQuoteCharacter()->willReturn('"');
 
         $this->connectionProphet->quote(',', Argument::cetera())->shouldBeCalled()->willReturn("','");
+        $this->connectionProphet->quote("'1'", null)->shouldBeCalled()->willReturn("'1'");
         $this->connectionProphet->quoteIdentifier(Argument::cetera())->will(function ($args) {
             return '"' . $args[0] . '"';
         });
@@ -368,6 +390,30 @@ class ExpressionBuilderTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCa
         $this->subject->inSet('aField', ':dcValue1');
     }
 
+    /**
+     * @test
+     */
+    public function inSetForMssql()
+    {
+        $databasePlatform = $this->prophesize(MockPlatform::class);
+        $databasePlatform->getName()->willReturn('mssql');
+        $databasePlatform->getStringLiteralQuoteCharacter()->willReturn('\'');
+
+        $this->connectionProphet->quote('1', null)->shouldBeCalled()->willReturn("'1'");
+        $this->connectionProphet->quote('1,%', null)->shouldBeCalled()->willReturn("'1,%'");
+        $this->connectionProphet->quote('%,1', null)->shouldBeCalled()->willReturn("'%,1'");
+        $this->connectionProphet->quote('%,1,%', null)->shouldBeCalled()->willReturn("'%,1,%'");
+        $this->connectionProphet->quoteIdentifier(Argument::cetera())->will(function ($args) {
+            return '[' . $args[0] . ']';
+        });
+
+        $this->connectionProphet->getDatabasePlatform()->willReturn($databasePlatform->reveal());
+
+        $result = $this->subject->inSet('aField', "'1'");
+
+        $this->assertSame("([aField] = '1') OR ([aField] LIKE '1,%') OR ([aField] LIKE '%,1') OR ([aField] LIKE '%,1,%')", $result);
+    }
+
     /**
      * @test
      */
diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbQueryParser.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbQueryParser.php
index eb3d8faea56d..079362763e7a 100644
--- a/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbQueryParser.php
+++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbQueryParser.php
@@ -364,7 +364,7 @@ class Typo3DbQueryParser
                     } else {
                         return $this->queryBuilder->expr()->inSet(
                             $tableName . '.' . $columnName,
-                            $this->queryBuilder->createNamedParameter($value)
+                            $this->queryBuilder->quote($value)
                         );
                     }
                 } else {
-- 
GitLab