/** * Perform migration * * @param string $base Absolute base path (parent of assets folder). Will default to BASE_PATH * @return int Number of files successfully migrated */ public function run($base = null) { if (empty($base)) { $base = BASE_PATH; } // Check if the File dataobject has a "Filename" field. // If not, cannot migrate /** @skipUpgrade */ if (!DB::get_schema()->hasField('File', 'Filename')) { return 0; } // Set max time and memory limit increase_time_limit_to(); increase_memory_limit_to(); // Loop over all files $count = 0; $originalState = Versioned::get_reading_mode(); Versioned::set_stage(Versioned::DRAFT); $filenameMap = $this->getFilenameArray(); foreach ($this->getFileQuery() as $file) { // Get the name of the file to import $filename = $filenameMap[$file->ID]; $success = $this->migrateFile($base, $file, $filename); if ($success) { $count++; } } Versioned::set_reading_mode($originalState); return $count; }
public function testFilter() { if (DB::get_conn() instanceof MySQLDatabase) { $baseQuery = FulltextFilterTest_DataObject::get(); $this->assertEquals(3, $baseQuery->count(), "FulltextFilterTest_DataObject count does not match."); // First we'll text the 'SearchFields' which has been set using an array $search = $baseQuery->filter("SearchFields:fulltext", 'SilverStripe'); $this->assertEquals(1, $search->count()); $search = $baseQuery->exclude("SearchFields:fulltext", "SilverStripe"); $this->assertEquals(2, $search->count()); // Now we'll run the same tests on 'OtherSearchFields' which should yield the same resutls // but has been set using a string. $search = $baseQuery->filter("OtherSearchFields:fulltext", 'SilverStripe'); $this->assertEquals(1, $search->count()); $search = $baseQuery->exclude("OtherSearchFields:fulltext", "SilverStripe"); $this->assertEquals(2, $search->count()); // Search on a single field $search = $baseQuery->filter("ColumnE:fulltext", 'Dragons'); $this->assertEquals(1, $search->count()); $search = $baseQuery->exclude("ColumnE:fulltext", "Dragons"); $this->assertEquals(2, $search->count()); } else { $this->markTestSkipped("FulltextFilter only supports MySQL syntax."); } }
public function testMultipleRowInsert() { $query = SQLInsert::create('"SQLInsertTestBase"'); $query->addRow(array('"Title"' => 'First Object', '"Age"' => 10, '"Description"' => 'First the worst')); $query->addRow(array('"Title"' => 'Second object', '"Age"' => 12)); $sql = $query->sql($parameters); // Only test this case if using the default query builder if (get_class(DB::get_conn()->getQueryBuilder()) === 'SilverStripe\\ORM\\Connect\\DBQueryBuilder') { $this->assertSQLEquals('INSERT INTO "SQLInsertTestBase" ("Title", "Age", "Description") VALUES (?, ?, ?), (?, ?, ?)', $sql); } $this->assertEquals(array('First Object', 10, 'First the worst', 'Second object', 12, null), $parameters); $query->execute(); $this->assertEquals(2, DB::affected_rows()); // Check inserted objects are correct $firstObject = DataObject::get_one('SQLInsertTestBase', array('"Title"' => 'First Object'), false); $this->assertNotEmpty($firstObject); $this->assertEquals($firstObject->Title, 'First Object'); $this->assertEquals($firstObject->Age, 10); $this->assertEquals($firstObject->Description, 'First the worst'); $secondObject = DataObject::get_one('SQLInsertTestBase', array('"Title"' => 'Second object'), false); $this->assertNotEmpty($secondObject); $this->assertEquals($secondObject->Title, 'Second object'); $this->assertEquals($secondObject->Age, 12); $this->assertEmpty($secondObject->Description); }
public function testCreateWithTransaction() { if (DB::get_conn()->supportsTransactions() == true) { DB::get_conn()->transactionStart(); $obj = new TransactionTest_Object(); $obj->Title = 'First page'; $obj->write(); $obj = new TransactionTest_Object(); $obj->Title = 'Second page'; $obj->write(); //Create a savepoint here: DB::get_conn()->transactionSavepoint('rollback'); $obj = new TransactionTest_Object(); $obj->Title = 'Third page'; $obj->write(); $obj = new TransactionTest_Object(); $obj->Title = 'Fourth page'; $obj->write(); //Revert to a savepoint: DB::get_conn()->transactionRollback('rollback'); DB::get_conn()->transactionEnd(); $first = DataObject::get('TransactionTest_Object', "\"Title\"='First page'"); $second = DataObject::get('TransactionTest_Object', "\"Title\"='Second page'"); $third = DataObject::get('TransactionTest_Object', "\"Title\"='Third page'"); $fourth = DataObject::get('TransactionTest_Object', "\"Title\"='Fourth page'"); //These pages should be in the system $this->assertTrue(is_object($first) && $first->exists()); $this->assertTrue(is_object($second) && $second->exists()); //These pages should NOT exist, we reverted to a savepoint: $this->assertFalse(is_object($third) && $third->exists()); $this->assertFalse(is_object($fourth) && $fourth->exists()); } else { $this->markTestSkipped('Current database does not support transactions'); } }
/** * (non-PHPdoc) * @see DBField::requireField() */ public function requireField() { $charset = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'charset'); $collation = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'collation'); $parts = array('datatype' => 'varchar', 'precision' => $this->size, 'character set' => $charset, 'collate' => $collation, 'arrayValue' => $this->arrayValue); $values = array('type' => 'varchar', 'parts' => $parts); DB::require_field($this->tableName, $this->name, $values); }
public function requireField() { // @todo: Remove mysql-centric logic from this $charset = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'charset'); $collation = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'collation'); $values = array('type' => 'set', 'parts' => array('enums' => $this->enum, 'character set' => $charset, 'collate' => $collation, 'default' => $this->default, 'table' => $this->tableName, 'arrayValue' => $this->arrayValue)); DB::require_field($this->tableName, $this->name, $values); }
/** * (non-PHPdoc) * @see DBField::requireField() */ public function requireField() { $charset = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'charset'); $collation = Config::inst()->get('SilverStripe\\ORM\\Connect\\MySQLDatabase', 'collation'); $parts = ['datatype' => 'mediumtext', 'character set' => $charset, 'collate' => $collation, 'default' => $this->defaultVal, 'arrayValue' => $this->arrayValue]; $values = ['type' => 'text', 'parts' => $parts]; DB::require_field($this->tableName, $this->name, $values); }
public function testTablesAreCreated() { $tables = DB::table_list(); $check = array('versionableextensionstest_dataobject_test1_live', 'versionableextensionstest_dataobject_test2_live', 'versionableextensionstest_dataobject_test3_live', 'versionableextensionstest_dataobject_test1_versions', 'versionableextensionstest_dataobject_test2_versions', 'versionableextensionstest_dataobject_test3_versions'); // Check that the right tables exist foreach ($check as $tableName) { $this->assertContains($tableName, array_keys($tables), 'Contains table: ' . $tableName); } }
public function run($request) { $migrated = FileMigrationHelper::singleton()->run(); if ($migrated) { DB::alteration_message("{$migrated} File DataObjects upgraded", "changed"); } else { DB::alteration_message("No File DataObjects need upgrading", "notice"); } }
public function requireField() { // HACK: MSSQL does not support double so we're using float instead // @todo This should go into MSSQLDatabase ideally somehow if (DB::get_conn() instanceof MySQLDatabase) { DB::require_field($this->tableName, $this->name, "double"); } else { DB::require_field($this->tableName, $this->name, "float"); } }
public function conditionSQL(&$parameters) { $parameters = array(); // Ignore empty conditions $where = $this->whereQuery->getWhere(); if (empty($where)) { return null; } // Allow database to manage joining of conditions $sql = DB::get_conn()->getQueryBuilder()->buildWhereFragment($this->whereQuery, $parameters); return preg_replace('/^\\s*WHERE\\s*/i', '', $sql); }
function testValidAlternativeDatabaseName() { $prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_'; Config::inst()->update('SilverStripe\\Control\\Director', 'environment_type', 'dev'); $this->assertTrue(DB::valid_alternative_database_name($prefix . 'tmpdb1234567')); $this->assertFalse(DB::valid_alternative_database_name($prefix . 'tmpdb12345678')); $this->assertFalse(DB::valid_alternative_database_name('tmpdb1234567')); $this->assertFalse(DB::valid_alternative_database_name('random')); $this->assertFalse(DB::valid_alternative_database_name('')); Config::inst()->update('SilverStripe\\Control\\Director', 'environment_type', 'live'); $this->assertFalse(DB::valid_alternative_database_name($prefix . 'tmpdb1234567')); Config::inst()->update('SilverStripe\\Control\\Director', 'environment_type', 'dev'); }
/** * Returns the manifest of all classes which are present in the database. * * @param string $class Class name to check enum values for ClassName field * @param boolean $includeUnbacked Flag indicating whether or not to include * types that don't exist as implemented classes. By default these are excluded. * @return array List of subclasses */ public static function getValidSubClasses($class = 'SilverStripe\\CMS\\Model\\SiteTree', $includeUnbacked = false) { if (is_string($class) && !class_exists($class)) { return array(); } $class = self::class_name($class); if ($includeUnbacked) { $table = DataObject::getSchema()->tableName($class); $classes = DB::get_schema()->enumValuesForField($table, 'ClassName'); } else { $classes = static::subclassesFor($class); } return $classes; }
protected function excludeMany(DataQuery $query) { $this->model = $query->applyRelation($this->relation); $values = $this->getValue(); $comparisonClause = DB::get_conn()->comparisonClause($this->getDbName(), null, false, true, $this->getCaseSensitive(), true); $parameters = array(); foreach ($values as $value) { $parameters[] = $this->getMatchPattern($value); } // Since query connective is ambiguous, use AND explicitly here $count = count($values); $predicate = implode(' AND ', array_fill(0, $count, $comparisonClause)); return $query->where(array($predicate => $parameters)); }
public function testBasicUpdate() { $query = SQLUpdate::create()->setTable('"SQLUpdateTestBase"')->assign('"Description"', 'Description 1a')->addWhere(array('"Title" = ?' => 'Object 1')); $sql = $query->sql($parameters); // Check SQL $this->assertSQLEquals('UPDATE "SQLUpdateTestBase" SET "Description" = ? WHERE ("Title" = ?)', $sql); $this->assertEquals(array('Description 1a', 'Object 1'), $parameters); // Check affected rows $query->execute(); $this->assertEquals(1, DB::affected_rows()); // Check item updated $item = DataObject::get_one('SQLUpdateTestBase', array('"Title"' => 'Object 1')); $this->assertEquals('Description 1a', $item->Description); }
protected function foreignIDFilter($id = null) { if ($id === null) { $id = $this->getForeignID(); } // Apply relation filter $key = DataObject::getSchema()->sqlColumnForField($this->dataClass(), $this->getForeignKey()); if (is_array($id)) { return array("{$key} IN (" . DB::placeholders($id) . ")" => $id); } else { if ($id !== null) { return array($key => $id); } } return null; }
public function testAffectedRows() { if (!DB::get_connector() instanceof PDOConnector) { $this->markTestSkipped('This test requires the current DB connector is PDO'); } $query = new SQLUpdate('MySQLDatabaseTest_Data'); $query->setAssignments(array('Title' => 'New Title')); // Test update which affects no rows $query->setWhere(array('Title' => 'Bob')); $result = $query->execute(); $this->assertInstanceOf('SilverStripe\\ORM\\Connect\\PDOQuery', $result); $this->assertEquals(0, DB::affected_rows()); // Test update which affects some rows $query->setWhere(array('Title' => 'First Item')); $result = $query->execute(); $this->assertInstanceOf('SilverStripe\\ORM\\Connect\\PDOQuery', $result); $this->assertEquals(1, DB::affected_rows()); }
/** * Link this group set to a specific member. * * Recursively selects all groups applied to this member, as well as any * parent groups of any applied groups * * @param array|integer $id (optional) An ID or an array of IDs - if not provided, will use the current * ids as per getForeignID * @return array Condition In array(SQL => parameters format) */ public function foreignIDFilter($id = null) { if ($id === null) { $id = $this->getForeignID(); } // Find directly applied groups $manyManyFilter = parent::foreignIDFilter($id); $query = new SQLSelect('"Group_Members"."GroupID"', '"Group_Members"', $manyManyFilter); $groupIDs = $query->execute()->column(); // Get all ancestors, iteratively merging these into the master set $allGroupIDs = array(); while ($groupIDs) { $allGroupIDs = array_merge($allGroupIDs, $groupIDs); $groupIDs = DataObject::get("SilverStripe\\Security\\Group")->byIDs($groupIDs)->column("ParentID"); $groupIDs = array_filter($groupIDs); } // Add a filter to this DataList if (!empty($allGroupIDs)) { $allGroupIDsPlaceholders = DB::placeholders($allGroupIDs); return array("\"Group\".\"ID\" IN ({$allGroupIDsPlaceholders})" => $allGroupIDs); } else { return array('"Group"."ID"' => 0); } }
protected function overrideField($obj, $fieldName, $value, $fixtures = null) { $class = get_class($obj); $table = DataObject::getSchema()->tableForField($class, $fieldName); $value = $this->parseValue($value, $fixtures); DB::manipulate(array($table => array("command" => "update", "id" => $obj->ID, "class" => $class, "fields" => array($fieldName => $value)))); $obj->{$fieldName} = $value; }
public function requireField() { $parts = array('datatype' => 'tinyint', 'precision' => 1, 'sign' => 'unsigned', 'null' => 'not null', 'default' => $this->defaultVal, 'arrayValue' => $this->arrayValue); $values = array('type' => 'boolean', 'parts' => $parts); DB::require_field($this->tableName, $this->name, $values); }
/** * Add default records to database. This function is called whenever the * database is built, after the database tables have all been created. Overload * this to add default records when the database is built, but make sure you * call parent::requireDefaultRecords(). * * @uses DataExtension->requireDefaultRecords() */ public function requireDefaultRecords() { $defaultRecords = $this->stat('default_records'); if (!empty($defaultRecords)) { $hasData = DataObject::get_one($this->class); if (!$hasData) { $className = $this->class; foreach ($defaultRecords as $record) { $obj = $this->model->{$className}->newObject($record); $obj->write(); } DB::alteration_message("Added default records to {$className} table", "created"); } } // Let any extentions make their own database default data $this->extend('requireDefaultRecords', $dummy); }
require_once 'core/startup/ErrorControlChain.php'; require_once 'core/startup/ParameterConfirmationToken.php'; // Prepare tokens and execute chain $reloadToken = ParameterConfirmationToken::prepare_tokens(array('isTest', 'isDev', 'flush')); $chain = new ErrorControlChain(); $chain->then(function ($chain) use($reloadToken) { // If no redirection is necessary then we can disable error supression if (!$reloadToken) { $chain->setSuppression(false); } // Load in core require_once 'core/Core.php'; // Connect to database global $databaseConfig; if ($databaseConfig) { DB::connect($databaseConfig); } // Check if a token is requesting a redirect if (!$reloadToken) { return; } // Otherwise, we start up the session if needed if (!isset($_SESSION) && Session::request_contains_session_id()) { Session::start(); } // Next, check if we're in dev mode, or the database doesn't have any security data, or we are admin if (Director::isDev() || !Security::database_is_ready() || Permission::check('ADMIN')) { return $reloadToken->reloadWithToken(); } // Fail and redirect the user to the login page $loginPage = Director::absoluteURL(Security::config()->login_url);
public function testTransactions() { $conn = DB::get_conn(); if (!$conn->supportsTransactions()) { $this->markTestSkipped("DB Doesn't support transactions"); return; } // Test that successful transactions are comitted $obj = new DatabaseTest_MyObject(); $failed = false; $conn->withTransaction(function () use(&$obj) { $obj->MyField = 'Save 1'; $obj->write(); }, function () use(&$failed) { $failed = true; }); $this->assertEquals('Save 1', DatabaseTest_MyObject::get()->first()->MyField); $this->assertFalse($failed); // Test failed transactions are rolled back $ex = null; $failed = false; try { $conn->withTransaction(function () use(&$obj) { $obj->MyField = 'Save 2'; $obj->write(); throw new Exception("error"); }, function () use(&$failed) { $failed = true; }); } catch (Exception $ex) { } $this->assertTrue($failed); $this->assertEquals('Save 1', DatabaseTest_MyObject::get()->first()->MyField); $this->assertInstanceOf('Exception', $ex); $this->assertEquals('error', $ex->getMessage()); }
/** * Get the list of classnames, including obsolete classes. * * If table or name are not set, or if it is not a valid field on the given table, * then only known classnames are returned. * * Values cached in this method can be cleared via `DBClassName::clear_classname_cache();` * * @return array */ public function getEnumObsolete() { // Without a table or field specified, we can only retrieve known classes $table = $this->getTable(); $name = $this->getName(); if (empty($table) || empty($name)) { return $this->getEnum(); } // Ensure the table level cache exists if (empty(self::$classname_cache[$table])) { self::$classname_cache[$table] = array(); } // Check existing cache if (!empty(self::$classname_cache[$table][$name])) { return self::$classname_cache[$table][$name]; } // Get all class names $classNames = $this->getEnum(); if (DB::get_schema()->hasField($table, $name)) { $existing = DB::query("SELECT DISTINCT \"{$name}\" FROM \"{$table}\"")->column(); $classNames = array_unique(array_merge($classNames, $existing)); } // Cache and return self::$classname_cache[$table][$name] = $classNames; return $classNames; }
/** * Reset the testing database's schema. * @param $includeExtraDataObjects If true, the extraDataObjects tables will also be included */ public function resetDBSchema($includeExtraDataObjects = false) { if (self::using_temp_db()) { DataObject::reset(); // clear singletons, they're caching old extension info which is used in DatabaseAdmin->doBuild() Injector::inst()->unregisterAllObjects(); $dataClasses = ClassInfo::subclassesFor('SilverStripe\\ORM\\DataObject'); array_shift($dataClasses); DB::quiet(); $schema = DB::get_schema(); $extraDataObjects = $includeExtraDataObjects ? $this->extraDataObjects : null; $schema->schemaUpdate(function () use($dataClasses, $extraDataObjects) { foreach ($dataClasses as $dataClass) { // Check if class exists before trying to instantiate - this sidesteps any manifest weirdness if (class_exists($dataClass)) { $SNG = singleton($dataClass); if (!$SNG instanceof TestOnly) { $SNG->requireTable(); } } } // If we have additional dataobjects which need schema, do so here: if ($extraDataObjects) { foreach ($extraDataObjects as $dataClass) { $SNG = singleton($dataClass); if (singleton($dataClass) instanceof DataObject) { $SNG->requireTable(); } } } }); ClassInfo::reset_db_cache(); singleton('SilverStripe\\ORM\\DataObject')->flushCache(); } }
public function testFilterByNull() { $list = DataObjectTest_Fan::get(); // Force DataObjectTest_Fan/fan5::Email to empty string $fan5id = $this->idFromFixture('DataObjectTest_Fan', 'fan5'); DB::prepared_query("UPDATE \"DataObjectTest_Fan\" SET \"Email\" = '' WHERE \"ID\" = ?", array($fan5id)); // Filter by null email $nullEmails = $list->filter('Email', null); $this->assertDOSEquals(array(array('Name' => 'Stephen'), array('Name' => 'Mitch')), $nullEmails); // Filter by non-null $nonNullEmails = $list->filter('Email:not', null); $this->assertDOSEquals(array(array('Name' => 'Damian', 'Email' => '*****@*****.**'), array('Name' => 'Richard', 'Email' => '*****@*****.**'), array('Name' => 'Hamish')), $nonNullEmails); // Filter by empty only $emptyOnly = $list->filter('Email', ''); $this->assertDOSEquals(array(array('Name' => 'Hamish')), $emptyOnly); // Non-empty only. This should include null values, since ExactMatchFilter works around // the caveat that != '' also excludes null values in ANSI SQL-92 behaviour. $nonEmptyOnly = $list->filter('Email:not', ''); $this->assertDOSEquals(array(array('Name' => 'Damian', 'Email' => '*****@*****.**'), array('Name' => 'Richard', 'Email' => '*****@*****.**'), array('Name' => 'Stephen'), array('Name' => 'Mitch')), $nonEmptyOnly); // Filter by many including null, empty string, and non-empty $items1 = $list->filter('Email', array(null, '', '*****@*****.**')); $this->assertDOSEquals(array(array('Name' => 'Damian', 'Email' => '*****@*****.**'), array('Name' => 'Stephen'), array('Name' => 'Mitch'), array('Name' => 'Hamish')), $items1); // Filter exclusion of above list $items2 = $list->filter('Email:not', array(null, '', '*****@*****.**')); $this->assertDOSEquals(array(array('Name' => 'Richard', 'Email' => '*****@*****.**')), $items2); // Filter by many including empty string and non-empty $items3 = $list->filter('Email', array('', '*****@*****.**')); $this->assertDOSEquals(array(array('Name' => 'Damian', 'Email' => '*****@*****.**'), array('Name' => 'Hamish')), $items3); // Filter by many including empty string and non-empty // This also relies no the workaround for null comparison as in the $nonEmptyOnly test $items4 = $list->filter('Email:not', array('', '*****@*****.**')); $this->assertDOSEquals(array(array('Name' => 'Richard', 'Email' => '*****@*****.**'), array('Name' => 'Stephen'), array('Name' => 'Mitch')), $items4); // Filter by many including empty string and non-empty // The extra null check isn't necessary, but check that this doesn't fail $items5 = $list->filterAny(array('Email:not' => array('', '*****@*****.**'), 'Email' => null)); $this->assertDOSEquals(array(array('Name' => 'Richard', 'Email' => '*****@*****.**'), array('Name' => 'Stephen'), array('Name' => 'Mitch')), $items5); // Filter by null or empty values $items6 = $list->filter('Email', array(null, '')); $this->assertDOSEquals(array(array('Name' => 'Stephen'), array('Name' => 'Mitch'), array('Name' => 'Hamish')), $items6); }
public function requireField() { $parts = array('datatype' => 'time', 'arrayValue' => $this->arrayValue); $values = array('type' => 'time', 'parts' => $parts); DB::require_field($this->tableName, $this->name, $values); }
/** * Writes the fixture into the database directly using a database manipulation. * Does not use blueprints. Only supports tables with a primary key. * * @param String $table Existing database table name * @param String $identifier Unique identifier for this fixture type * @param Array $data Map of properties * @return Int Database identifier */ public function createRaw($table, $identifier, $data) { $fields = array(); foreach ($data as $fieldName => $fieldVal) { $fields["\"{$fieldName}\""] = $this->parseValue($fieldVal); } $insert = new SQLInsert("\"{$table}\"", $fields); $insert->execute(); $id = DB::get_generated_id($table); $this->fixtures[$table][$identifier] = $id; return $id; }
public function requireField() { $parts = array('datatype' => 'float', 'null' => 'not null', 'default' => $this->defaultVal, 'arrayValue' => $this->arrayValue); $values = array('type' => 'float', 'parts' => $parts); DB::require_field($this->tableName, $this->name, $values); }
/** * Applies matches for several values, either as inclusive or exclusive * * @param DataQuery $query * @param bool $inclusive True if this is inclusive, or false if exclusive * @return DataQuery */ protected function manyFilter(DataQuery $query, $inclusive) { $this->model = $query->applyRelation($this->relation); $caseSensitive = $this->getCaseSensitive(); // Check values for null $field = $this->getDbName(); $values = $this->getValue(); if (empty($values)) { throw new \InvalidArgumentException("Cannot filter {$field} against an empty set"); } $hasNull = in_array(null, $values, true); if ($hasNull) { $values = array_filter($values, function ($value) { return $value !== null; }); } $connective = ''; if (empty($values)) { $predicate = ''; } elseif ($caseSensitive === null) { // For queries using the default collation (no explicit case) we can use the WHERE .. NOT IN .. syntax, // providing simpler SQL than many WHERE .. AND .. fragments. $column = $this->getDbName(); $placeholders = DB::placeholders($values); if ($inclusive) { $predicate = "{$column} IN ({$placeholders})"; } else { $predicate = "{$column} NOT IN ({$placeholders})"; } } else { // Generate reusable comparison clause $comparisonClause = DB::get_conn()->comparisonClause($this->getDbName(), null, true, !$inclusive, $this->getCaseSensitive(), true); $count = count($values); if ($count > 1) { $connective = $inclusive ? ' OR ' : ' AND '; $conditions = array_fill(0, $count, $comparisonClause); $predicate = implode($connective, $conditions); } else { $predicate = $comparisonClause; } } // Always check for null when doing exclusive checks (either AND IS NOT NULL / OR IS NULL) // or when including the null value explicitly (OR IS NULL) if ($hasNull || !$inclusive) { // If excluding values which don't include null, or including // values which include null, we should do an `OR IS NULL`. // Otherwise we are excluding values that do include null, so `AND IS NOT NULL`. // Simplified from (!$inclusive && !$hasNull) || ($inclusive && $hasNull); $isNull = !$hasNull || $inclusive; $nullCondition = DB::get_conn()->nullCheckClause($field, $isNull); // Determine merge strategy if (empty($predicate)) { $predicate = $nullCondition; } else { // Merge null condition with predicate if ($isNull) { $nullCondition = " OR {$nullCondition}"; } else { $nullCondition = " AND {$nullCondition}"; } // If current predicate connective doesn't match the same as the null connective // make sure to group the prior condition if ($connective && ($connective === ' OR ') !== $isNull) { $predicate = "({$predicate})"; } $predicate .= $nullCondition; } } return $query->where(array($predicate => $values)); }