@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Introduce Calendar "UTC Epoch" columns for query windowing

Summary:
Ref T10747. Currently, Calendar events are mostly epoch-based and cheat a little bit for all-day events.

This already felt a little flimsy, and can't reasonably accommodate the full range of `.ics` events, which include "floating" events (e.g., occurs at 3PM regardless of timezone, like "Tea Time").

As a secondary issue, we identify instances of a recurring event by instance number (1, 2, 3, etc.). This can't accommodate the full range of `.ics` events, which include arbitrary additional "RDATE" events (e.g., recurrs every week, and also on these specific extra days).

However, we do need to store some epoch information so we can do query windowing: when the user looks at "October 2016", we want to select the smallest number of events that we can from the database initially, before refining them down to generate instances. We can't reasonably query the actual dates no matter how we store them because this depends on computing things like UNTIL, COUNT, initial dates, whether events are recurring or not, timezones, etc.

Instead, when we save an event compute the earliest second it occurs on in UTC and the latest second it occurs on in UTC. We can then query for a small superset of possible events in "October 2016" for any viewer pretty easily.

Also, start laying the groundwork for using fewer epochs in the rest of the code, and for reducing the role of sequence indexes (I plan to keep some sequences indexes around, probably, since they're nice in the UI, but not all child events will have indexes since there's no index for an RDATE event).

This doesn't migrate existing events yet or actually read these new columns -- that will come later once the new code is a little more solid.

Test Plan:
- Ran `bin/storage upgrade`.
- Created a new event.
- Saved an existing event.
- Viewed database, saw sensible-looking "UTC Epoch" values.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10747

Differential Revision: https://secure.phabricator.com/D16652

+129 -5
+8
resources/sql/autopatches/20161003.cal.01.utcepoch.sql
··· 1 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 2 + ADD utcInitialEpoch INT UNSIGNED NOT NULL; 3 + 4 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 5 + ADD utcUntilEpoch INT UNSIGNED; 6 + 7 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 8 + ADD utcInstanceEpoch INT UNSIGNED;
+121 -5
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 41 41 42 42 protected $spacePHID; 43 43 44 + protected $utcInitialEpoch; 45 + protected $utcUntilEpoch; 46 + protected $utcInstanceEpoch; 47 + 44 48 private $parentEvent = self::ATTACHABLE; 45 49 private $invitees = self::ATTACHABLE; 46 50 47 51 private $viewerDateFrom; 48 52 private $viewerDateTo; 53 + private $viewerTimezone; 49 54 50 55 // Frequency Constants 51 56 const FREQUENCY_DAILY = 'daily'; ··· 298 303 $zone); 299 304 } 300 305 306 + $this->viewerTimezone = $viewer->getTimezoneIdentifier(); 307 + 301 308 return $this; 302 309 } 303 310 ··· 323 330 return $dst->format('U'); 324 331 } 325 332 333 + public function updateUTCEpochs() { 334 + // The "intitial" epoch is the start time of the event, in UTC. 335 + $start_date = $this->newStartDateTime() 336 + ->setViewerTimezone('UTC'); 337 + $start_epoch = $start_date->getEpoch(); 338 + $this->setUTCInitialEpoch($start_epoch); 339 + 340 + // The "until" epoch is the last UTC epoch on which any instance of this 341 + // event occurs. For infinitely recurring events, it is `null`. 342 + 343 + if (!$this->getIsRecurring()) { 344 + $end_date = $this->newEndDateTime() 345 + ->setViewerTimezone('UTC'); 346 + $until_epoch = $end_date->getEpoch(); 347 + } else { 348 + $until_epoch = null; 349 + $until_date = $this->newUntilDateTime() 350 + ->setViewerTimezone('UTC'); 351 + if ($until_date) { 352 + $duration = $this->newDuration(); 353 + $until_epoch = id(new PhutilCalendarRelativeDateTime()) 354 + ->setOrigin($until_date) 355 + ->setDuration($duration) 356 + ->getEpoch(); 357 + } 358 + } 359 + $this->setUTCUntilEpoch($until_epoch); 360 + 361 + // The "instance" epoch is a property of instances of recurring events. 362 + // It's the original UTC epoch on which the instance started. Usually that 363 + // is the same as the start date, but they may be different if the instance 364 + // has been edited. 365 + 366 + // The ICS format uses this value (original start time) to identify event 367 + // instances, and must do so because it allows additional arbitrary 368 + // instances to be added (with "RDATE"). 369 + 370 + $instance_epoch = null; 371 + $instance_date = $this->newInstanceDateTime(); 372 + if ($instance_date) { 373 + $instance_epoch = $instance_date 374 + ->setViewerTimezone('UTC') 375 + ->getEpoch(); 376 + } 377 + $this->setUTCInstanceEpoch($instance_epoch); 378 + 379 + return $this; 380 + } 381 + 326 382 public function save() { 327 383 if (!$this->mailKey) { 328 384 $this->mailKey = Filesystem::readRandomCharacters(20); 329 385 } 386 + 387 + $this->updateUTCEpochs(); 330 388 331 389 return parent::save(); 332 390 } ··· 363 421 'instanceOfEventPHID' => 'phid?', 364 422 'sequenceIndex' => 'uint32?', 365 423 'isStub' => 'bool', 424 + 'utcInitialEpoch' => 'epoch', 425 + 'utcUntilEpoch' => 'epoch?', 426 + 'utcInstanceEpoch' => 'epoch?', 366 427 ), 367 428 self::CONFIG_KEY_SCHEMA => array( 368 429 'key_date' => array( ··· 372 433 'columns' => array('instanceOfEventPHID', 'sequenceIndex'), 373 434 'unique' => true, 374 435 ), 436 + 'key_epoch' => array( 437 + 'columns' => array('utcInitialEpoch', 'utcUntilEpoch'), 438 + ), 439 + 'key_rdate' => array( 440 + 'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'), 441 + 'unique' => true, 442 + ), 375 443 ), 376 444 self::CONFIG_SERIALIZATION => array( 377 445 'recurrenceFrequency' => self::SERIALIZATION_JSON, ··· 641 709 $modified = $this->getDateModified(); 642 710 $modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified); 643 711 644 - $date_start = $this->getDateFrom(); 645 - $date_start = PhutilCalendarAbsoluteDateTime::newFromEpoch($date_start); 646 - 647 - $date_end = $this->getDateTo(); 648 - $date_end = PhutilCalendarAbsoluteDateTime::newFromEpoch($date_end); 712 + $date_start = $this->newStartDateTime(); 713 + $date_end = $this->newEndDateTime(); 649 714 650 715 if ($this->getIsAllDay()) { 651 716 $date_start->setIsAllDay(true); ··· 719 784 return $node; 720 785 } 721 786 787 + public function newStartDateTime() { 788 + $epoch = $this->getDateFrom(); 789 + return $this->newDateTimeFromEpoch($epoch); 790 + } 791 + 792 + public function newEndDateTime() { 793 + $epoch = $this->getDateTo(); 794 + return $this->newDateTimeFromEpoch($epoch); 795 + } 796 + 797 + public function newUntilDateTime() { 798 + $epoch = $this->getRecurrenceEndDate(); 799 + if (!$epoch) { 800 + return null; 801 + } 802 + return $this->newDateTimeFromEpoch($epoch); 803 + } 804 + 805 + public function newDuration() { 806 + return id(new PhutilCalendarDuration()) 807 + ->setSeconds($this->getDuration()); 808 + } 809 + 810 + public function newInstanceDateTime() { 811 + if (!$this->getIsRecurring()) { 812 + return null; 813 + } 814 + 815 + $epochs = $this->getParent()->getSequenceIndexEpochs( 816 + new PhabricatorUser(), 817 + $this->getSequenceIndex(), 818 + $this->getDuration()); 819 + 820 + $epoch = $epochs['dateFrom']; 821 + return $this->newDateTimeFromEpoch($epoch); 822 + } 823 + 824 + private function newDateTimeFromEpoch($epoch) { 825 + $datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($epoch); 826 + 827 + $viewer_timezone = $this->viewerTimezone; 828 + if ($viewer_timezone) { 829 + $datetime->setViewerTimezone($viewer_timezone); 830 + } 831 + 832 + if ($this->getIsAllDay()) { 833 + $datetime->setIsAllDay(true); 834 + } 835 + 836 + return $datetime; 837 + } 722 838 723 839 724 840 /* -( Markup Interface )--------------------------------------------------- */