Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
8ffa0ec
Complete vector casting and MariaDB similarity queries
binaryfire Sep 10, 2026
86efebc
Use direct Stringable construction internally
binaryfire Sep 10, 2026
9d3294b
Preserve health-page CSS when Blade theme directives exist
binaryfire Sep 10, 2026
7e9b603
Fix decimal number summaries and complete Number typing and coverage
binaryfire Sep 10, 2026
a87a661
Constrain model identifier collection class annotations
binaryfire Sep 10, 2026
dc7a53a
Preserve raw queue payloads and index failed-job counts
binaryfire Sep 10, 2026
bbef3fc
Remove unused host port publications from container CI jobs
binaryfire Sep 10, 2026
7ec1be3
Preserve non-error HTTP responses when retrying asynchronously
binaryfire Sep 10, 2026
4ba0cfa
Complete upstream fractional translation selector coverage
binaryfire Sep 10, 2026
a842eff
Document conditional CORS middleware bypass
binaryfire Sep 10, 2026
8468c7b
Explain deferred queue callback ownership beside the lifecycle listener
binaryfire Sep 10, 2026
e5e0842
Verify parallel compiled-view cleanup through its real lifecycle
binaryfire Sep 10, 2026
3279294
Port collection merge union type assertions
binaryfire Sep 10, 2026
f7645d6
Document queue startup, pop and exception-release events
binaryfire Sep 10, 2026
74a0928
Complete HTTP response JSON flag types
binaryfire Sep 10, 2026
d90bc91
Document HTTP response decoding flags and defaults
binaryfire Sep 10, 2026
184d7a4
Preserve explicit handlers when deferred dispatch is disabled
binaryfire Sep 10, 2026
d9ac030
Reject malformed CSRF tokens through the existing validator
binaryfire Sep 10, 2026
b5166be
Complete request-forgery exclusion coverage
binaryfire Sep 10, 2026
8b71c51
Document the supported CSRF middleware replacements
binaryfire Sep 10, 2026
7e6a6dc
Fix raw expression selection and preserve authorization attributes
binaryfire Sep 10, 2026
591c9dd
Document explicit expression aliases and policy selections
binaryfire Sep 10, 2026
94eb720
Complete notification failure event regression coverage
binaryfire Sep 10, 2026
b56155e
Preserve character boundaries and encodings in string helpers
binaryfire Sep 10, 2026
e5af2ab
Document fluent resource collection key preservation
binaryfire Sep 10, 2026
86d9ed8
Preserve morph-map opt-in and integer model aliases
binaryfire Sep 10, 2026
c6f8467
Honor inherited relation exclusion when serializing jobs
binaryfire Sep 10, 2026
a5055cc
Document queued relationship restoration and morph aliases
binaryfire Sep 10, 2026
03a89f7
Release owned database lock rows after expiration
binaryfire Sep 10, 2026
f3defb7
Preserve enum defaults in request input types
binaryfire Sep 10, 2026
23cc5ad
Document and test multiple MySQL index hints
binaryfire Sep 10, 2026
162ba36
Verify unique listener lock release with real lock state
binaryfire Sep 11, 2026
53d8305
Correct unique listener scope and queue example imports
binaryfire Sep 11, 2026
9923311
Complete associative eager-load key coverage and documentation
binaryfire Sep 11, 2026
92a321c
Complete typed cache getter coverage and documentation
binaryfire Sep 11, 2026
a660254
Align the column starting-value description with upstream
binaryfire Sep 11, 2026
5e80d6b
Format summarized zero using the requested locale
binaryfire Sep 11, 2026
f83605d
Compare vector casts at database storage precision
binaryfire Sep 11, 2026
30073bc
Complete enum notification channel resolution coverage
binaryfire Sep 11, 2026
1d40ae0
Explain the queue identifier storage regression
binaryfire Sep 11, 2026
0598d24
Select Arabic-Indic digits explicitly in number tests
binaryfire Sep 11, 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
12 changes: 0 additions & 12 deletions .github/workflows/databases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ jobs:
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
ports:
- 3306:3306
options: >-
--health-cmd "mysqladmin ping -h localhost"
--health-interval 10s
Expand Down Expand Up @@ -79,8 +77,6 @@ jobs:
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
ports:
- 3306:3306
options: >-
--health-cmd "mysqladmin ping -h localhost"
--health-interval 10s
Expand Down Expand Up @@ -135,8 +131,6 @@ jobs:
env:
MARIADB_ROOT_PASSWORD: password
MARIADB_DATABASE: testing
ports:
- 3306:3306
options: >-
--health-cmd "healthcheck.sh --connect --innodb_initialized"
--health-interval 10s
Expand Down Expand Up @@ -191,8 +185,6 @@ jobs:
env:
MARIADB_ROOT_PASSWORD: password
MARIADB_DATABASE: testing
ports:
- 3306:3306
options: >-
--health-cmd "healthcheck.sh --connect --innodb_initialized"
--health-interval 10s
Expand Down Expand Up @@ -247,8 +239,6 @@ jobs:
env:
POSTGRES_PASSWORD: password
POSTGRES_DB: testing
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
Expand Down Expand Up @@ -303,8 +293,6 @@ jobs:
env:
POSTGRES_PASSWORD: password
POSTGRES_DB: testing
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/redis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ jobs:
services:
redis:
image: redis:8
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
Expand Down Expand Up @@ -253,8 +251,6 @@ jobs:
services:
valkey:
image: valkey/valkey:9
ports:
- 6379:6379
options: >-
--health-cmd "valkey-cli ping"
--health-interval 10s
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/reverb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ jobs:
services:
redis:
image: redis:8
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/scout.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ jobs:
env:
MEILI_MASTER_KEY: secret
MEILI_NO_ANALYTICS: true
ports:
- 7700:7700
options: >-
--health-cmd "curl -f http://localhost:7700/health"
--health-interval 10s
Expand Down Expand Up @@ -78,8 +76,6 @@ jobs:
env:
TYPESENSE_API_KEY: secret
TYPESENSE_DATA_DIR: /tmp
ports:
- 8108:8108

container:
image: ghcr.io/hypervel/components-ci:php${{ matrix.php }}-swoole6.2.2
Expand Down
17 changes: 14 additions & 3 deletions src/auth/src/AuthServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Hypervel\Contracts\Auth\Access\Gate as GateContract;
use Hypervel\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Hypervel\Contracts\Config\Repository as ConfigRepository;
use Hypervel\Contracts\Database\Query\Expression as ExpressionContract;
use Hypervel\Core\Events\AfterWorkerStart;
use Hypervel\Database\Eloquent\Builder as EloquentBuilder;
use Hypervel\Database\Eloquent\Collection as EloquentCollection;
Expand Down Expand Up @@ -135,9 +136,19 @@ private function registerQueryBuilderMacros(GateContract $gate): void
$casts = [];

foreach ($resolvedAbilities as [$ability, $alias]) {
$this->addSelect([
$alias => $queryGate->select($ability, $this),
]);
$selection = $queryGate->select($ability, $this);

if ($selection instanceof ExpressionContract) {
// Raw authorization selections must retain the model columns, like subqueries do.
if ($this->getQuery()->columns === null) {
$this->select($this->getQuery()->getDefaultSelectColumn());
}

$this->selectExpression($selection, $alias);
} else {
$this->addSelect([$alias => $selection]);
}

$casts[$alias] = 'bool';
}

Expand Down
2 changes: 1 addition & 1 deletion src/bus/src/Dispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ protected function pushCommandToQueue(Queue $queue, mixed $command): mixed
public function dispatchAfterResponse(mixed $command, mixed $handler = null): void
{
if (! $this->allowsDispatchingAfterResponses) {
$this->dispatchSync($command);
$this->dispatchSync($command, $handler);

return;
}
Expand Down
24 changes: 9 additions & 15 deletions src/cache/src/DatabaseLock.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,24 +119,18 @@ public function acquire(): bool
*/
public function release(): bool
{
if ($this->isOwnedByCurrentProcess()) {
try {
$this->connection()->table($this->table)
->where('key', $this->name)
->where('owner', $this->owner)
->delete();

try {
return $this->connection()->table($this->table)
->where('key', $this->name)
->where('owner', $this->owner)
->delete() > 0;
} catch (Throwable $e) {
if ($this->causedByConcurrencyError($e)) {
return true;
} catch (Throwable $e) {
if ($this->causedByConcurrencyError($e)) {
return true;
}

throw $e;
}
}

return false;
throw $e;
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/console/src/QuestionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ protected function writePrompt(OutputInterface $output, Question $question): voi
*/
protected function ensureEndsWithPunctuation(string $string): string
{
if (! (new Stringable($string))->endsWith(['?', ':', '!', '.'])) {
if ((new Stringable($string))->doesntEndWith(['?', ':', '!', '.'])) {
return "{$string}:";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class EnsurePunctuation
*/
public function __invoke(string $string): string
{
if (! (new Stringable($string))->endsWith(['.', '?', '!', ':'])) {
if ((new Stringable($string))->doesntEndWith(['.', '?', '!', ':'])) {
return "{$string}.";
}

Expand Down
25 changes: 13 additions & 12 deletions src/contracts/src/Database/ModelIdentifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@

namespace Hypervel\Contracts\Database;

use Hypervel\Database\Eloquent\Collection;
use Hypervel\Database\Eloquent\Relations\Relation;

/**
* NOTE: Do not use constructor property promotion here.
* Do not use constructor property promotion here.
*
* The order these properties are declared in is part of the serialized output,
* and Laravel expects that exact order. If this class is switched to constructor
* property promotion, PHP will change the property declaration order and the
* serialized string will no longer match Laravel.
*
* Keep these properties explicitly declared in this exact order:
* class, id, relations, connection, collectionClass.
*/
class ModelIdentifier
{
Expand All @@ -25,9 +23,9 @@ class ModelIdentifier
protected static bool $useMorphMap = false;

/**
* The class name of the model, or its morph-map alias when enabled.
* The class name of the model, or its string or integer morph-map alias when enabled.
*/
public ?string $class;
public int|string|null $class;

/**
* The unique identifier of the model.
Expand All @@ -51,7 +49,7 @@ class ModelIdentifier
/**
* The class name of the model collection.
*
* @var null|class-string<\Hypervel\Database\Eloquent\Collection>
* @var null|class-string<Collection>
*/
public ?string $collectionClass = null;

Expand All @@ -66,7 +64,7 @@ class ModelIdentifier
public function __construct(?string $class, mixed $id, array $relations, ?string $connection = null)
{
if ($class !== null && static::$useMorphMap) {
$class = (string) Relation::getMorphAlias($class);
$class = Relation::getMorphAlias($class);
}

$this->class = $class;
Expand All @@ -78,7 +76,7 @@ public function __construct(?string $class, mixed $id, array $relations, ?string
/**
* Specify the collection class that should be used when serializing / restoring collections.
*
* @param null|class-string $collectionClass
* @param null|class-string<Collection> $collectionClass
*/
public function useCollectionClass(?string $collectionClass): static
{
Expand All @@ -92,11 +90,14 @@ public function useCollectionClass(?string $collectionClass): static
*/
public function getClass(): ?string
{
if ($this->class === null) {
return null;
$class = $this->class;

if (static::$useMorphMap && $class !== null) {
$class = Relation::getMorphedModel($class) ?? $class;
}

return Relation::getMorphedModel($this->class) ?? $this->class;
// Unmapped integer aliases still follow the nullable-string getter contract.
return $class === null ? null : (string) $class;
}

/**
Expand Down
116 changes: 116 additions & 0 deletions src/database/src/Eloquent/Casts/AsVector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

declare(strict_types=1);

namespace Hypervel\Database\Eloquent\Casts;

use Hypervel\Contracts\Database\Eloquent\Castable;
use Hypervel\Contracts\Database\Eloquent\CastsAttributes;
use Hypervel\Contracts\Database\Eloquent\ComparesCastableAttributes;
use Hypervel\Contracts\Database\Query\Expression as ExpressionContract;
use Hypervel\Contracts\Support\Arrayable;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Database\Query\Expression;
use Hypervel\Database\Query\Grammars\MariaDbGrammar;
use Hypervel\Support\Str;
use InvalidArgumentException;
use JsonException;

class AsVector implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @return CastsAttributes<array<int, float>, array<int, float>|Arrayable<int, float>>
*/
public static function castUsing(array $arguments): CastsAttributes
{
return new class implements CastsAttributes, ComparesCastableAttributes {
// Eloquent otherwise caches an assigned Arrayable and returns it instead of a float array.
public bool $withoutObjectCaching = true;

/**
* Transform the attribute from the underlying model values.
*
* @return null|array<int, float>
*
* @throws JsonException
*/
public function get(Model $model, string $key, mixed $value, array $attributes): ?array
{
if ($value === null) {
return null;
}

$grammar = $model->getConnection()->getQueryGrammar();

// Decode a MariaDB expression assigned to the model before it is persisted...
if ($value instanceof ExpressionContract) {
$value = Str::between($value->getValue($grammar), "('", "')");

return array_map(floatval(...), json_decode($value, true, flags: JSON_THROW_ON_ERROR));
}

// MariaDB vector columns return little-endian float32 bytes...
if ($grammar instanceof MariaDbGrammar) {
return array_values(unpack('g*', $value));
}

// PostgreSQL (pgvector) returns JSON text...
return array_map(floatval(...), json_decode($value, true, flags: JSON_THROW_ON_ERROR));
}

/**
* Transform the attribute to its underlying model values.
*
* @return array<string, null|Expression|string>
*
* @throws InvalidArgumentException
* @throws JsonException
*/
public function set(Model $model, string $key, mixed $value, array $attributes): array
{
if ($value === null) {
return [$key => null];
}

if ($value instanceof Arrayable) {
$value = $value->toArray();
}

if (! is_array($value)) {
throw new InvalidArgumentException(
sprintf('The [%s] attribute must be an array of floats or an Arrayable instance.', $key)
);
}

$vector = json_encode(array_values(array_map(floatval(...), $value)), JSON_THROW_ON_ERROR);

// MariaDB requires vectors to be converted from JSON text server-side...
return [
$key => $model->getConnection()->getQueryGrammar() instanceof MariaDbGrammar
? new Expression("vec_fromtext('{$vector}')")
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
: $vector,
];
}

/**
* Determine if the given values are equal.
*
* @throws JsonException
*/
public function compare(Model $model, string $key, mixed $firstValue, mixed $secondValue): bool
{
$first = $this->get($model, $key, $firstValue, []);
$second = $this->get($model, $key, $secondValue, []);

if ($first === null || $second === null) {
return $first === $second;
}

// Both supported engines store 32-bit floats, so compare the values as they are stored.
return pack('g*', ...$first) === pack('g*', ...$second);
}
};
}
}
Loading