debug('Begin', ['=start' => 'GetFlows', '=timed' => 1, 'from' => $this->startDate, 'to' => $this->endDate]); // Check dates are valid and get a Ymd format of the day *following* the specified one. // This is so we can find activities *before* this date and thereby capture activities that // occurred *on* the given date. $getValidDate = fn(?string $input) => $input ? (new DateTimeImmutable($input))->modify('+1 day')->format('Ymd'): FALSE; $endDate = NULL; $startDate = $getValidDate($this->startDate); if (!$startDate) { throw new CRM_Core_Exception(E::ts("Cannot parse given start date.")); } if (!empty($this->endDate)) { $endDate = $getValidDate($this->endDate); if (!$endDate) { throw new CRM_Core_Exception(E::ts("Cannot parse given end date.")); } if (!($endDate > $startDate)) { throw new CRM_Core_Exception(E::ts("This version of CiviCRM does not support twisting the space time continuum. End date cannot be before start date.")); } } else if ($startDate > date('Ymd', strtotime('tomorrow'))) { throw new CRM_Core_Exception(E::ts("This version of CiviCRM does not support predicting the future. Start time cannot be in the future.")); } // $result['windowFunctionsSupported'] = $this->windowFunctionsSupported(); // if ($result['windowFunctionsSupported']) { if ($this->windowFunctionsSupported()) { $this->solveWithWindowFunctions($result, $startDate, $endDate); } else { // TODO: make a slow version that doesn't need window functions. // $this->solveWithoutWindowFunctions($result); throw new CRM_Core_Exception("your database does not support Window functions."); } Civi::log()->debug("Complete.", ['=' => 'set', 'result' => $result->getArrayCopy()]); } /** * Does the database support window functions in the way we need? * * MariaDB 10.2+ does. */ protected function windowFunctionsSupported() { $cache = \CRM_Utils_Cache::create(['type' => ['SqlGroup'], 'name' => 'contactcats']); $supported = $cache->get('windowFunctionsSupported'); // Will return default if cached value expired. $supported = null; if ($supported === NULL) { // Need to test for window functions. $supported = FALSE; // Prevent Civi handling possible execption $handler = set_exception_handler(fn() => false); try { \CRM_Core_DAO::executeQuery("CREATE TEMPORARY TABLE contactcats_window_check (i int unsigned not null, x int unsigned not null)"); \CRM_Core_DAO::executeQuery("INSERT INTO contactcats_window_check VALUES (1, 1), (2, 1), (3, 2);"); $rows = \CRM_Core_DAO::executeQuery("SELECT x, ROW_NUMBER() OVER ( PARTITION BY (x) ORDER BY i DESC) rn FROM contactcats_window_check ORDER BY x, rn")->fetchAll(); $supported = ($rows === [ [ 'x' => "1", 'rn' => "1"], [ 'x' => "1", 'rn' => "2"], [ 'x' => "2", 'rn' => "1"] ]) ; } catch (\Exception $e) { // Silent fail. } // Restore exception handler set_exception_handler($handler); $cache->set('windowFunctionsSupported', $supported); } return $supported; } /** * */ protected function solveWithWindowFunctions(Result $result, string $startDateYmd, ?string $endDateYmd) { $sql = << [$startDateYmd, 'Int'], 2 => [$endDateYmd, 'Int'], ])->fetchAll(); // Don't use exchange array so as not to gazump non-array data(?) foreach ($data as $row) { $result[] = [ 'from_category_id' => (int) $row['new_category_id'], 'to_category_id' => (int) $row['to_category_id'], 'contact_count' => (int) $row['contact_count'], ]; } } }