Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
96b35ba
Keep Unicode trailing-whitespace trimming linear
binaryfire Oct 28, 2026
35d063b
Reconcile Redis limiter metadata and remaining-header coverage
binaryfire Oct 28, 2026
f3aa684
Recognize every callable form before resolving SQS provider names
binaryfire Oct 28, 2026
ba99cf8
Preserve explicit SES credential providers over inherited keys
binaryfire Oct 28, 2026
5d2a8d1
Support named S3 credential providers with pooled clients
binaryfire Oct 28, 2026
0516ed4
Add case-insensitive single-column schema inspection
binaryfire Oct 28, 2026
6708671
Preserve polymorphic identity in through relationships and aggregates
binaryfire Oct 28, 2026
29bfa97
Exclude refreshed attributes when replicating models
binaryfire Oct 28, 2026
718f8be
Cover pivot range filters in relationship writes
binaryfire Oct 28, 2026
ec671d1
Align compiled route scheme handling and coverage
binaryfire Oct 28, 2026
c1a1c97
Accept property maps in fake assertions
binaryfire Oct 28, 2026
63fc13d
Support percentage memory limits and persistent scheduler interrupts
binaryfire Oct 28, 2026
91b3d8a
Handle directories removed during parallel test cleanup
binaryfire Oct 28, 2026
68d7387
Preserve exact remaining counts in Redis throttles
binaryfire Oct 28, 2026
a12868f
Match only readable instance properties in fake assertions
binaryfire Oct 28, 2026
659b780
Resolve schema columns by exact name before case folding
binaryfire Oct 28, 2026
c92768c
Link S3 provider customization to the porting guide
binaryfire Oct 28, 2026
5b5db16
Leave view-test facade cleanup to the shared subscriber
binaryfire Oct 28, 2026
b4af723
Align Redis limiter test doubles with count-based replies
binaryfire Oct 28, 2026
81de24b
Match readable null property hooks in fake assertions
binaryfire Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 2 additions & 16 deletions src/console/src/Commands/ScheduleInterruptCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ class ScheduleInterruptCommand extends Command
/**
* The console signature name.
*/
protected ?string $signature = 'schedule:interrupt
{--minutes=1 : TTL in minutes for the interrupt signal (minimum 1)}
';
protected ?string $signature = 'schedule:interrupt';

/**
* The console command description.
Expand All @@ -38,19 +36,7 @@ public function __construct(
*/
public function handle(): int
{
$minutes = filter_var($this->option('minutes'), FILTER_VALIDATE_INT);

if ($minutes === false || $minutes < 1) {
$this->components->error('The --minutes option must be an integer of at least 1.');

return self::FAILURE;
}

$this->cache->put(
'hypervel:schedule:interrupt',
true,
Date::now()->addMinutes($minutes)
);
$this->cache->forever('hypervel:schedule:interrupt', Date::now()->getTimestampMs());

$this->components->info('Broadcasting schedule interrupt signal.');

Expand Down
45 changes: 14 additions & 31 deletions src/console/src/Commands/ScheduleRunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,17 @@ class ScheduleRunCommand extends Command
protected bool $eventsRan = false;

/**
* Check if scheduler should stop.
* Whether this schedule run has been interrupted.
*/
protected bool $shouldStop = false;
protected bool $interrupted = false;

/**
* Whether a termination signal has requested shutdown.
*/
protected bool $shouldQuit = false;

/**
* Last time the stopped state was checked.
* Last time the interruption state was checked.
*/
protected ?CarbonInterface $lastChecked = null;

Expand Down Expand Up @@ -147,7 +147,7 @@ public function handle(
Cache $cache,
ExceptionHandler $handler,
): void {
$this->shouldQuit = false;
$this->startedAt = Date::now();
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

$this->schedule = $schedule;
$this->dispatcher = $dispatcher;
Expand All @@ -168,15 +168,13 @@ public function handle(
$this->listenForSignals();

try {
$this->clearShouldStop();

$noEventsAlerted = false;
while (! $this->shouldQuit && ! $this->shouldStop()) {
$startedAt = Date::now();
while (! $this->shouldQuit && ! $this->shouldInterrupt()) {
$now = Date::now();

$this->runEvents(
$this->schedule->dueEventsAt($this->hypervel, $startedAt),
$startedAt
$this->schedule->dueEventsAt($this->hypervel, $now),
$now
);

if (! $this->eventsRan && ! $noEventsAlerted && ! $this->option('whisper')) {
Expand Down Expand Up @@ -215,14 +213,8 @@ protected function stop(): void
*/
protected function runOnce(): void
{
$this->startedAt = Date::now();

$events = $this->schedule->dueEventsAt($this->hypervel, $this->startedAt);

if ($events->contains->isRepeatable()) {
$this->clearShouldStop();
}

// The finite --once path runs inside Waiter; signal-wait coroutines keep
// that scheduler coroutine alive, so mutex signal cleanup is long-run only.
(new Waiter(-1))->wait(function () use ($events) {
Expand Down Expand Up @@ -250,7 +242,7 @@ protected function repeatEvents(Collection $events): void
$paused = $this->isPaused();

foreach ($events as $event) {
if ($this->shouldStop()) {
if ($this->shouldInterrupt()) {
return;
}

Expand Down Expand Up @@ -586,7 +578,7 @@ protected function isPaused(): bool
/**
* Determine if the schedule run should be interrupted.
*/
protected function shouldStop(): bool
protected function shouldInterrupt(): bool
{
if (! Schedule::$interruptible) {
return false;
Expand All @@ -596,23 +588,14 @@ protected function shouldStop(): bool
$this->lastChecked = Date::now();
}

if ($this->shouldStop || abs($this->lastChecked->diffInSeconds()) < 1) {
return $this->shouldStop;
// The loop and repeating events check more often; avoid a cache round trip on every check.
if ($this->interrupted || abs($this->lastChecked->diffInSeconds()) < 1) {
return $this->interrupted;
}

$this->lastChecked = Date::now();

return $this->shouldStop = (bool) $this->cache->get('hypervel:schedule:interrupt', false);
}

/**
* Clear the interrupt cache.
*/
protected function clearShouldStop(): void
{
$this->cache->forget('hypervel:schedule:interrupt');

$this->shouldStop = false;
return $this->interrupted = $this->schedule->hasBeenInterruptedSince($this->startedAt);
}

/**
Expand Down
14 changes: 14 additions & 0 deletions src/console/src/Scheduling/Schedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,20 @@ public static function withoutInterruptionPolling(): void
static::$interruptible = false;
}

/**
* Determine if the schedule has been interrupted since the given time.
*/
public function hasBeenInterruptedSince(DateTimeInterface $time): bool
{
if (! static::$interruptible) {
return false;
}

$interruptedAt = Container::getInstance()->make(Cache::class)->get('hypervel:schedule:interrupt');

return is_numeric($interruptedAt) && (int) $interruptedAt >= Date::instance($time)->getTimestampMs();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Flush all static state.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ Documentation: https://hypervel.org/docs/database
- Eloquent expression plucks use `Query\Builder::pluckWithColumn()` to retain the returned field name for casts and accessors. Custom query builders overriding `pluck()` must also override `pluckWithColumn()` to customize this path. String-based Eloquent plucks still call `pluck()`.
- Eloquent's protected `refreshSavedAttributes()` method receives the raw attribute values each write stored, so configured attributes are refreshed from the row that write changed. Models that override this method must accept the `array $attributes` argument.
- Eloquent `updateFrom()` maintains the model's `updated_at` timestamp, like `update()`. Laravel forwards this method without adding a timestamp.
- `BelongsToMany` records pivot filters in an ordered `$pivotConstraints` list instead of Laravel's `$pivotWheres`, `$pivotWhereIns`, and `$pivotWhereNulls` properties. Relationship extensions should use the `wherePivot*` methods instead of accessing those arrays.
- `BelongsToMany` records pivot filters in an ordered `$pivotConstraints` list instead of Laravel's `$pivotWheres`, `$pivotWhereIns`, `$pivotWhereNulls`, and `$pivotWhereBetweens` properties. Relationship extensions should use the `wherePivot*` methods instead of accessing those arrays.

Ported from: https://github.com/laravel/framework
10 changes: 3 additions & 7 deletions src/database/src/Eloquent/Concerns/HasRelationships.php
Original file line number Diff line number Diff line change
Expand Up @@ -476,18 +476,14 @@ protected function guessBelongsToRelation(): string
* Create a pending has-many-through or has-one-through relationship.
*
* @template TIntermediateModel of Model
* @template TLocalRelationship of HasOneOrMany<TIntermediateModel, $this, *>
*
* @param HasMany<TIntermediateModel, covariant $this>|HasOne<TIntermediateModel, covariant $this>|string $relationship
* @param string|TLocalRelationship $relationship
* @return (
* $relationship is string
* ? PendingHasThroughRelationship<Model, $this, HasOneOrMany<Model, $this, *>>
* : (
* $relationship is HasMany<TIntermediateModel, $this>
* ? PendingHasThroughRelationship<TIntermediateModel, $this, HasMany<TIntermediateModel, $this>>
* : PendingHasThroughRelationship<TIntermediateModel, $this, HasOne<TIntermediateModel, $this>>
* )
* : PendingHasThroughRelationship<TIntermediateModel, $this, TLocalRelationship>
* )
* @phpstan-ignore conditionalType.alwaysFalse (template covariance limitation with conditional return types)
*/
public function through(string|HasOneOrMany $relationship): PendingHasThroughRelationship
{
Expand Down
1 change: 1 addition & 0 deletions src/database/src/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -2338,6 +2338,7 @@ public function replicate(?array $except = null): static
$this->getCreatedAtColumn(),
$this->getUpdatedAtColumn(),
...$this->uniqueIds(),
...$this->refreshes,
'hypervel_through_key',
]));

Expand Down
18 changes: 12 additions & 6 deletions src/database/src/Eloquent/PendingHasThroughRelationship.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Hypervel\Database\Eloquent\Relations\HasOne;
use Hypervel\Database\Eloquent\Relations\HasOneOrMany;
use Hypervel\Database\Eloquent\Relations\HasOneThrough;
use Hypervel\Database\Eloquent\Relations\MorphMany;
use Hypervel\Database\Eloquent\Relations\MorphOneOrMany;
use Hypervel\Support\Str;
use Hypervel\Support\Stringable;
Expand Down Expand Up @@ -52,30 +53,31 @@ public function __construct(Model $rootModel, HasOneOrMany $localRelationship)
*
* @template TRelatedModel of Model
*
* @param (callable(TIntermediateModel): (HasMany<TRelatedModel, TIntermediateModel>|HasOne<TRelatedModel, TIntermediateModel>|MorphOneOrMany<TRelatedModel, TIntermediateModel, *>))|string $callback
* @param (callable(TIntermediateModel): (HasMany<TRelatedModel, TIntermediateModel>|HasOne<TRelatedModel, TIntermediateModel>|MorphMany<TRelatedModel, TIntermediateModel>|MorphOneOrMany<TRelatedModel, TIntermediateModel, *>))|string $callback
* @return (
* $callback is string
* ? HasManyThrough<Model, TIntermediateModel, TDeclaringModel>|HasOneThrough<Model, TIntermediateModel, TDeclaringModel>
* : (
* TLocalRelationship is HasMany<TIntermediateModel, TDeclaringModel>
* TLocalRelationship is HasMany<TIntermediateModel, TDeclaringModel>|MorphMany<TIntermediateModel, TDeclaringModel>
* ? HasManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
* : (
* $callback is callable(TIntermediateModel): HasMany<TRelatedModel, TIntermediateModel>
* $callback is callable(TIntermediateModel): (HasMany<TRelatedModel, TIntermediateModel>|MorphMany<TRelatedModel, TIntermediateModel>)
* ? HasManyThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
* : HasOneThrough<TRelatedModel, TIntermediateModel, TDeclaringModel>
* )
* )
* )
*/
public function has(callable|string $callback): mixed
public function has(callable|string $callback): HasManyThrough|HasOneThrough
{
if (is_string($callback)) {
$callback = fn () => $this->localRelationship->getRelated()->{$callback}();
}

$distantRelation = $callback($this->localRelationship->getRelated());

if ($distantRelation instanceof HasMany || $this->localRelationship instanceof HasMany) {
if ($distantRelation instanceof HasMany || $distantRelation instanceof MorphMany
|| $this->localRelationship instanceof HasMany || $this->localRelationship instanceof MorphMany) {
$returnedRelation = $this->rootModel->hasManyThrough(
$distantRelation->getRelatedClass(),
$this->localRelationship->getRelatedClass(),
Expand All @@ -96,7 +98,11 @@ public function has(callable|string $callback): mixed
}

if ($this->localRelationship instanceof MorphOneOrMany) {
$returnedRelation->where($this->localRelationship->getQualifiedMorphType(), $this->localRelationship->getMorphClass());
$returnedRelation->whereMorphClass($this->localRelationship->getQualifiedMorphType(), $this->localRelationship->getMorphClass());
}

if ($distantRelation instanceof MorphOneOrMany) {
$returnedRelation->whereMorphClass($distantRelation->getQualifiedMorphType(), $distantRelation->getMorphClass());
}

return $returnedRelation;
Expand Down
6 changes: 5 additions & 1 deletion src/database/src/Eloquent/Relations/HasManyThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class HasManyThrough extends HasOneOrManyThrough
*/
public function one(): HasOneThrough
{
return HasOneThrough::noConstraints(fn () => new HasOneThrough(
$relation = HasOneThrough::noConstraints(fn () => new HasOneThrough(
tap($this->getQuery(), fn (Builder $query) => $query->getQuery()->joins = []),
$this->farParent,
$this->throughParent,
Expand All @@ -36,6 +36,10 @@ public function one(): HasOneThrough
$this->getLocalKeyName(),
$this->getSecondLocalKeyName(),
));

$relation->morphConstraints = $this->morphConstraints;

return $relation;
}

public function initRelation(array $models, string $relation): array
Expand Down
20 changes: 20 additions & 0 deletions src/database/src/Eloquent/Relations/HasOneOrManyThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ abstract class HasOneOrManyThrough extends Relation
*/
protected string $secondLocalKey;

/**
* Morph identities that also constrain one-of-many subqueries.
*
* @var array<string, string>
*/
protected array $morphConstraints = [];

/**
* Create a new has many through relationship instance.
*
Expand Down Expand Up @@ -105,6 +112,19 @@ public function addConstraints(): void
}
}

/**
* Constrain the relationship to records of the given morph class.
*
* @return $this
*/
public function whereMorphClass(string $column, string $morphClass): static
{
$this->morphConstraints[$column] = $morphClass;
$this->query->where($column, $morphClass);

return $this;
}

/**
* Set the join clause on the query.
*
Expand Down
4 changes: 4 additions & 0 deletions src/database/src/Eloquent/Relations/HasOneThrough.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ public function addOneOfManySubQueryConstraints(Builder $query, ?string $column
{
$query->addSelect([$this->getQualifiedFirstKeyName()]);

foreach ($this->morphConstraints as $morphColumn => $morphClass) {
$query->where($morphColumn, $morphClass);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// We need to join subqueries that aren't the inner-most subquery which is joined in the CanBeOneOfMany::ofMany method...
if ($this->getOneOfManySubQuery() !== null) {
// @phpstan-ignore argument.type (Builder param typed without template in inherited interface)
Expand Down
Loading
Loading