The Connection class removes trailing semicolons from SQL Statements, but also supports a setting to allow semicolons in statements. For some reason, SQL Server requires that a MERGE statement end in a semicolon. A good universal approach would be to change:

else {
  $this->expandArguments($query, $args);
  // To protect against SQL injection, Drupal only supports executing one
  // statement at a time.  Thus, the presence of a SQL delimiter (the
  // semicolon) is not allowed unless the option is set.  Allowing
  // semicolons should only be needed for special cases like defining a
  // function or stored procedure in SQL. Trim any trailing delimiter to
  // minimize false positives.
  $query = rtrim($query, ";  \t\n\r\0\x0B");
  if (strpos($query, ';') !== FALSE && empty($options['allow_delimiter_in_query'])) {
    throw new \InvalidArgumentException('; is not supported in SQL strings. Use only one statement at a time.');
  }
  $stmt = $this->prepareQuery($query);
  $stmt->execute($args, $options);
}

To:

else {
  $this->expandArguments($query, $args);
  // To protect against SQL injection, Drupal only supports executing one
  // statement at a time.  Thus, the presence of a SQL delimiter (the
  // semicolon) is not allowed unless the option is set.  Allowing
  // semicolons should only be needed for special cases like defining a
  // function or stored procedure in SQL. Trim any trailing delimiter to
  // minimize false positives unless delimiter is allowed.
  $whitespace = "  \t\n\r\0\x0B";
  if (empty($options['allow_delimiter_in_query'])) {
    $whitespace .= ';';
  }
  $query = rtrim($query, $whitespace);
  if (strpos($query, ';') !== FALSE && empty($options['allow_delimiter_in_query'])) {
    throw new \InvalidArgumentException('; is not supported in SQL strings. Use only one statement at a time.');
  }
  $stmt = $this->prepareQuery($query);
  $stmt->execute($args, $options);
}

Comments

Beakerboy created an issue. See original summary.

beakerboy’s picture

beakerboy’s picture

Issue summary: View changes
beakerboy’s picture

Issue summary: View changes
beakerboy’s picture

Assigned: beakerboy » Unassigned
Status: Active » Needs review
StatusFileSize
new1.15 KB
beakerboy’s picture

Status: Needs review » Needs work

I have the logic backwards, and it needs a test.

beakerboy’s picture

Issue summary: View changes
ravi.shankar’s picture

Issue tags: +Needs tests

Added needs tests tag.

beakerboy’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new2.8 KB

Patch and test for this issue.

beakerboy’s picture

StatusFileSize
new2.74 KB

Fix coding standard issues

daffie’s picture

Status: Needs review » Needs work

Patch looks good and needs some work:

  1. +++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
    @@ -297,4 +297,55 @@ public function testFilterComments($expected, $comment) {
    +      ->setMethods(['execute', 'prepare', 'setAttribute'])
    

    Why do you set the methods: "execute" and "setAttribute" to be replaced by a test double and then do not do that?

  2. +++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
    @@ -297,4 +297,55 @@ public function testFilterComments($expected, $comment) {
    +  public function testQueryTrim($expected, $query, $options) {
    

    I am missing the assertion that the generated query is equal to the expected query.

  3. +++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
    @@ -297,4 +297,55 @@ public function testFilterComments($expected, $comment) {
    +    $mock_pdo = $this->getMockBuilder('Drupal\Tests\Core\Database\Stub\StubPDO')
    

    This line can be rewritten to: "$mock_pdo = $this->getMockBuilder(StubPDO::class)".

@Beakerboy: Could you add an interdiff.txt with your patch file. I makes reviewing your patch a lot easier.

beakerboy’s picture

@daffie

1)The connection class calls these two methods during the execution path of the test. I guess I could replace the constructor of Connection with a stub so it does nothing. That would avoid calling setAttribute(). I can’t stub out the constructor because that’s how the StubPDO object gets in. Should I rewrite the constructor to just inject StubPDO and not set any attributes? I don’t see a way around the execute() function, since Connection::query() calls it immediately after calling PDO::prepare(). Let me know if there’s a better way.

2)The mock object performs the assertion. For example, if you change the dataProvider the test will return the following:

Drupal\Tests\Core\Database\ConnectionTest::testQueryTrim with data set "keep_trailing_semicolon_with_whitespace" ('SELECT * FROM test', 'SELECT * FROM test; ', array(true))
Expectation failed for method name is equal to 'prepare' when invoked 1 time(s)
Parameter 0 for invocation PDO::prepare('SELECT * FROM test;', null) does not match expected value.
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'SELECT * FROM test'
+'SELECT * FROM test;'

Again, let me know if there’s a better way.
3) Can do, but all other tests in this class use this format so I was following precedent.

beakerboy’s picture

Status: Needs work » Needs review
StatusFileSize
new2.9 KB
new932 bytes

Using ::class keyword. Awaiting advice on other two suggestions. Should there be a follow up to use the ::class keyword throughout this class?

daffie’s picture

Status: Needs review » Needs work

Tested with the patch on my local machine.

For my own comment #11.1: the methods "execute" and "setAttribute" need to be mocked and the default mocking is enough for this test.

For my own comment #11.2: AFAIK every test needs an assertion or the PHPUnit will mark the test as risky. In this case that does not happen. Evidently I was wrong.

+++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
@@ -297,4 +298,55 @@ public function testFilterComments($expected, $comment) {
+    $mock_pdo->expects($this->once())
+      ->method('prepare')
+      ->with($expected)
+      ->willReturnSelf();

Could we add a comment before this code with something like: "When the method query() is called, the method prepare() should be called with the expected query." Or something else that explains how the testing works.

@Beakerboy: Thank you for adding the interdiff.txt file. I do not think that there should be a followup for only this class, maybe one for all test classes. You can create one if you like.

beakerboy’s picture

Status: Needs work » Needs review
StatusFileSize
new3.01 KB
new709 bytes

Added a comment to the test...although one of the points of a “fluent interface“ is that the code itself should explain what’s going on.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

@Beakerboy: I am used to look for something like $this->assert* to see what the test is testing. This test does not have that. Helping other coders understand how a non standard test work is to me almost always welcome. Thank you for adding it.
All my points are addressed.
All code changes look good to me.
For me it is RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

We need to fix this in 9.1.x first.

beakerboy’s picture

Status: Needs work » Needs review
StatusFileSize
new3.04 KB

Patch for 9.1.x

daffie’s picture

Status: Needs review » Reviewed & tested by the community

Patch is the same as the one for 8.9.
Back to RTBC.

  • alexpott committed c9c0fc1 on 9.1.x
    Issue #3133798 by Beakerboy, daffie: Semicolon removed from query even...
alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed c9c0fc1e59 to 9.1.x and 79495e89d7 to 9.0.x. Thanks!
Committed 635b422 and pushed to 8.9.x. Thanks!

  • alexpott committed 79495e8 on 9.0.x
    Issue #3133798 by Beakerboy, daffie: Semicolon removed from query even...

  • alexpott committed 635b422 on 8.9.x
    Issue #3133798 by Beakerboy, daffie: Semicolon removed from query even...
krzysztof domański’s picture

The $trim_chars contains two ordinary spaces.

$trim_chars = " \t\n\r\0\x0B";

This was before the last commit so it is not the result of a previous change. Looks like there should be one ordinary space and one non-breaking space. I created #3143618: Replace space (U+0020) with No-Break Space (U+00A0) to minimize false positives.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.