Commit 29f6bc7d authored by Thomas Seiber's avatar Thomas Seiber
Browse files

Issue #3258802 by drunken monkey: Fixed errors when indexing fields with unknown types.

parent 686a86fd
Loading
Loading
Loading
Loading
+2 −0
Changes for CHANGELOG.txt: 2 added lines, 0 removed lines.
Original line number Diff line number Diff line
Search API 1.x, dev (xxxx-xx-xx):
---------------------------------
- #3258802 by drunken monkey: Fixed errors when indexing fields with unknown
  types.
- #3232736 by GuyPaddock, idebr, drunken monkey: Added query tags as an option
  to Views.
- #3188138 by drunken monkey, devad: Added an "Item URL" field to Views.
+25 −0
Changes for modules/search_api_db/src/Plugin/search_api/backend/Database.php: 25 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@

namespace Drupal\search_api_db\Plugin\search_api\backend;

use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
@@ -920,6 +921,18 @@ class Database extends BackendPluginBase implements AutocompleteBackendInterface
        return ['type' => 'int', 'size' => 'tiny'];

      default:
        try {
          $data_type = $this->getDataTypePluginManager()->createInstance($type);
          if ($data_type && !$data_type->isDefault()) {
            $fallback_type = $data_type->getFallbackType();
            if ($fallback_type != $type) {
              return $this->sqlType($fallback_type);
            }
          }
        }
        catch (PluginException $e) {
          // Ignore.
        }
        throw new SearchApiException("Unknown field type '$type'.");
    }
  }
@@ -1540,6 +1553,18 @@ class Database extends BackendPluginBase implements AutocompleteBackendInterface
        return $value ? 1 : 0;

      default:
        try {
          $data_type = $this->getDataTypePluginManager()->createInstance($type);
          if ($data_type && !$data_type->isDefault()) {
            $fallback_type = $data_type->getFallbackType();
            if ($fallback_type != $type) {
              return $this->convert($value, $fallback_type, $original_type, $index);
            }
          }
        }
        catch (PluginException $e) {
          // Ignore.
        }
        throw new SearchApiException("Unknown field type '$type'.");
    }
  }
+79 −0
Changes for modules/search_api_db/tests/src/Kernel/BackendTest.php: 79 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ namespace Drupal\Tests\search_api_db\Kernel;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Database\Database as CoreDatabase;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\Entity\Server;
@@ -20,6 +21,7 @@ use Drupal\search_api_db\DatabaseCompatibility\GenericDatabase;
use Drupal\search_api_db\Plugin\search_api\backend\Database;
use Drupal\search_api_db\Tests\DatabaseTestsTrait;
use Drupal\Tests\search_api\Kernel\BackendTestBase;
use Drupal\Tests\search_api\Kernel\TestLogger;

/**
 * Tests index and search capabilities using the Database search backend.
@@ -50,6 +52,15 @@ class BackendTest extends BackendTestBase {
   */
  protected $indexId = 'database_search_index';

  /**
   * The test logger installed in the container.
   *
   * Will throw expections whenever a warning or error is logged.
   *
   * @var \Drupal\Tests\search_api\Kernel\TestLogger
   */
  protected $logger;

  /**
   * {@inheritdoc}
   */
@@ -94,6 +105,19 @@ class BackendTest extends BackendTestBase {
    $index->save();
  }

  /**
   * {@inheritdoc}
   */
  public function register(ContainerBuilder $container): void {
    parent::register($container);

    // Set a logger that will throw exceptions when warnings/errors are logged.
    $this->logger = new TestLogger('');
    $container->set('logger.factory', $this->logger);
    $container->set('logger.channel.search_api', $this->logger);
    $container->set('logger.channel.search_api_db', $this->logger);
  }

  /**
   * {@inheritdoc}
   */
@@ -126,6 +150,7 @@ class BackendTest extends BackendTestBase {
    $this->regressionTest2873023();
    $this->regressionTest3199355();
    $this->regressionTest3225675();
    $this->regressionTest3258802();
  }

  /**
@@ -699,9 +724,17 @@ class BackendTest extends BackendTestBase {
  protected function regressionTest2925464() {
    $index = $this->getIndex();

    // Changing the field type and, thus, column type, will cause a database
    // error on MySQL and Postgres due to illegal integer values.
    if (in_array(\Drupal::database()->driver(), ['mysql', 'pgsql'])) {
      $this->logger->setExpectedErrors(1);
    }

    $index->getField('category')->setType('integer');
    $index->save();

    $this->logger->assertAllExpectedErrorsEncountered();

    $index->getField('category')->setType('string');
    $index->save();

@@ -886,6 +919,42 @@ class BackendTest extends BackendTestBase {
    $this->indexItems($this->indexId);
  }

  /**
   * Tests whether unknown field types are handled correctly.
   *
   * @see https://www.drupal.org/node/3258802
   */
  protected function regressionTest3258802(): void {
    $this->enableModules(['search_api_test']);

    $index = $this->getIndex();
    $type_field = $index->getField('type');
    $this->assertEquals('string', $type_field->getType());
    $type_field->setType('search_api_test_unsupported');
    $index->save();
    // No tasks should have been created.
    $task_manager = \Drupal::getContainer()->get('search_api.task_manager');
    $this->assertEquals(0, $task_manager->getTasksCount());
    // Reindexing should work fine.
    $index->clear();
    $this->assertEquals(5, $this->indexItems($this->indexId));

    $results = $index->query()->addCondition('type', 'article')->execute();
    $this->assertResults([4, 5], $results, 'Search with filter on field with unknown type');

    $index = $this->getIndex();
    $type_field = $index->getField('type');
    $this->assertEquals('search_api_test_unsupported', $type_field->getType());
    $type_field->setType('string');
    $index->save();
    // No tasks should have been created.
    $tasks_count = $task_manager->getTasksCount();
    $this->assertEquals(0, $tasks_count);
    $this->indexItems($this->indexId);

    $this->disableModules(['search_api_test']);
  }

  /**
   * {@inheritdoc}
   */
@@ -904,6 +973,16 @@ class BackendTest extends BackendTestBase {
    return $index;
  }

  /**
   * {@inheritdoc}
   */
  protected function regressionTest2471509(): void {
    // As this test will log an exception, we need to take that into account.
    $this->logger->setExpectedErrors(2);
    parent::regressionTest2471509();
    $this->logger->assertAllExpectedErrorsEncountered();
  }

  /**
   * {@inheritdoc}
   */
+96 −0
Changes for tests/src/Kernel/TestLogger.php: 96 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\search_api\Kernel;

use Drupal\Core\Logger\LoggerChannel;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Logger\RfcLoggerTrait;
use Drupal\Core\Logger\RfcLogLevel;
use PHPUnit\Framework\Assert;
use Psr\Log\LoggerInterface;

/**
 * Provides a logger that throws exceptions when logging errors.
 */
class TestLogger extends LoggerChannel implements LoggerChannelFactoryInterface {

  use RfcLoggerTrait;

  /**
   * The number of currently expected errors.
   *
   * @var int
   */
  protected $expectedErrors = 0;

  /**
   * Retrieves the number of currently expected logged errors.
   *
   * @return int
   *   The number of currently expected logged errors.
   */
  public function getExpectedErrors(): int {
    return $this->expectedErrors;
  }

  /**
   * Sets an expectation for one or more errors to be logged.
   *
   * @param int $num_expected_errors
   *   The new expected number of errors.
   * @param int|null $expected_previous_setting
   *   (optional) The expected previous setting of the number of expected errors
   *   in order to make an assertion on that. Or NULL to skip the assertion.
   *
   * @return $this
   */
  public function setExpectedErrors(int $num_expected_errors = 1, ?int $expected_previous_setting = 0): self {
    if ($expected_previous_setting !== NULL) {
      Assert::assertEquals($expected_previous_setting, $this->expectedErrors);
    }
    $this->expectedErrors = $num_expected_errors;
    return $this;
  }

  /**
   * Asserts that all expected errors were in fact encountered.
   *
   * In other words, asserts that the currently expected number of errors to be
   * logged is 0.
   *
   * @return $this
   */
  public function assertAllExpectedErrorsEncountered(): self {
    Assert::assertEquals(0, $this->expectedErrors);
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function log($level, $message, array $context = []): void {
    if ($level < RfcLogLevel::INFO) {
      if ($this->expectedErrors > 0) {
        --$this->expectedErrors;
        return;
      }
      $message = strtr($message, $context);
      throw new \Exception($message);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function get($channel): LoggerChannelInterface {
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function addLogger(LoggerInterface $logger, $priority = 0): void {
  }

}