Skip to content
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,10 @@ These rules apply to all tests — new tests for framework work and ported tests

Test supported public behavior, meaningful branches, verified regressions, and realistic coroutine or worker-lifetime failures. Do not add production APIs, branches, or defensive machinery solely to make speculative states testable. Do not require invariants to survive deliberate framework escape hatches unless the public contract promises that behavior.

### Exceptions in tests

PHPUnit assertion failures, skips, and incomplete markers all extend `AssertionFailedError`, which extends `RuntimeException`. A test's `try`/`catch` must not turn any of them into success. When testing behavior after an exception, the catch must not be reachable by a failure that skips the behavior under test. Satisfy that by pinning the escaped exception by identity or message, or by catching a type that excludes every other failure the code path can produce.

### Directory layout

All tests live in `tests/{PackageName}/` (PascalCase). Tests that require external services go in `tests/Integration/{PackageName}/` — see Integration tests below. When only some integration tests for a package require one service, group them in `tests/Integration/{PackageName}/{ServiceName}/`. When every integration test for the package requires that service, keep them directly in the package directory.
Expand Down

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Documentation: https://hypervel.org/docs/database
- Laravel's external database pooler support uses a `::direct` connection suffix. Hypervel instead uses normal named connections for each endpoint and `migrations_connection` for schema and migration paths. This keeps direct and pooled endpoints as normal configured connections with their own pool settings, so Hypervel does not support Laravel's `::direct` suffix.
- Laravel's deprecated database-inspection forwarding helpers are intentionally not ported. Extensions can call `ConnectionInterface::getDriverTitle()` and `threadCount()` directly.
- Laravel's remaining directly deprecated Database compatibility forwarders are intentionally not ported. Use the current class-keyed factory resolver, schema blueprint and grammar APIs, and correctly named PostgreSQL truncation method instead.
- Laravel's Capsule manager exposes a `setFetchMode()` method that writes configuration its connections do not read. Hypervel omits this ineffective connection-wide setter; use `Query\Builder::fetchUsing()` for each query that needs a custom row shape.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- `make:migration` omits Laravel's deprecated `--fullpath` option and obsolete Composer constructor dependency because migration creation no longer dumps autoload files.
- `Blueprint::dropForeign()` widens Laravel's method signature with an optional constraint name when columns are supplied, allowing explicitly named foreign keys to be dropped portably across SQLite and the server databases. Custom `Blueprint` subclasses that override this method must accept the optional second argument.
- Eloquent models that override `CREATED_AT` or `UPDATED_AT` must declare the compatible `?string` constant type, such as `public const ?string UPDATED_AT = null;`. Laravel's constants are untyped, but omitting the type from an override in Hypervel causes a fatal error.
Expand Down
14 changes: 2 additions & 12 deletions src/database/src/Capsule/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Hypervel\Database\Query\Builder;
use Hypervel\Database\SimpleConnectionResolver;
use Hypervel\Support\Traits\CapsuleManagerTrait;
use PDO;

/**
* Standalone database manager for non-production use outside full application bootstrap.
Expand Down Expand Up @@ -60,8 +59,6 @@ public function __construct(?ContainerContract $container = null)
*/
protected function setupDefaultConfiguration(): void
{
$this->container['config']['database.fetch'] = PDO::FETCH_OBJ;

$this->container['config']['database.default'] = 'default';
}

Expand Down Expand Up @@ -144,15 +141,8 @@ public function bootEloquent(): void
}
}

/**
* Set the fetch mode for the database connections.
*/
public function setFetchMode(int $fetchMode): static
{
$this->container['config']['database.fetch'] = $fetchMode;

return $this;
}
// REMOVED: Capsule's setter writes unused configuration and cannot safely
// define a connection-wide row shape. Use Query\Builder::fetchUsing() per query.

/**
* Get the database manager instance.
Expand Down
32 changes: 20 additions & 12 deletions src/database/src/Concerns/BuildsQueries.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
use SortDirection;

/**
* @template TKey of array-key
* @template TValue
*
* @mixin \Hypervel\Database\Query\Builder
* @mixin \Hypervel\Database\Query\Builder<TKey, TValue>
*/
trait BuildsQueries
{
Expand All @@ -34,7 +35,7 @@ trait BuildsQueries
/**
* Chunk the results of the query.
*
* @param callable(\Hypervel\Support\Collection<int, TValue>, int): mixed $callback
* @param callable(\Hypervel\Support\Collection<TKey, TValue>, int): mixed $callback
*/
public function chunk(int $count, callable $callback): bool
{
Expand Down Expand Up @@ -112,8 +113,10 @@ public function chunkMap(callable $callback, int $count = 1000): Collection
public function each(callable $callback, int $count = 1000): bool
{
return $this->chunk($count, function ($results) use ($callback) {
foreach ($results as $key => $value) {
if ($callback($value, $key) === false) {
$position = 0;

foreach ($results as $value) {
if ($callback($value, $position++) === false) {
return false;
}
}
Expand All @@ -123,7 +126,7 @@ public function each(callable $callback, int $count = 1000): bool
/**
* Chunk the results of a query by comparing IDs.
*
* @param callable(\Hypervel\Support\Collection<int, TValue>, int): mixed $callback
* @param callable(\Hypervel\Support\Collection<TKey, TValue>, int): mixed $callback
*/
public function chunkById(int $count, callable $callback, ?string $column = null, ?string $alias = null): bool
{
Expand All @@ -133,7 +136,7 @@ public function chunkById(int $count, callable $callback, ?string $column = null
/**
* Chunk the results of a query by comparing IDs in descending order.
*
* @param callable(\Hypervel\Support\Collection<int, TValue>, int): mixed $callback
* @param callable(\Hypervel\Support\Collection<TKey, TValue>, int): mixed $callback
*/
public function chunkByIdDesc(int $count, callable $callback, ?string $column = null, ?string $alias = null): bool
{
Expand All @@ -143,7 +146,7 @@ public function chunkByIdDesc(int $count, callable $callback, ?string $column =
/**
* Chunk the results of a query by comparing IDs in a given order.
*
* @param callable(\Hypervel\Support\Collection<int, TValue>, int): mixed $callback
* @param callable(\Hypervel\Support\Collection<TKey, TValue>, int): mixed $callback
*/
public function orderedChunkById(int $count, callable $callback, ?string $column = null, ?string $alias = null, SortDirection|bool $descending = false): bool
{
Expand Down Expand Up @@ -220,8 +223,10 @@ public function orderedChunkById(int $count, callable $callback, ?string $column
public function eachById(callable $callback, int $count = 1000, ?string $column = null, ?string $alias = null): bool
{
return $this->chunkById($count, function ($results, $page) use ($callback, $count) {
foreach ($results as $key => $value) {
if ($callback($value, (($page - 1) * $count) + $key) === false) {
$position = 0;

foreach ($results as $value) {
if ($callback($value, (($page - 1) * $count) + $position++) === false) {
return false;
}
}
Expand Down Expand Up @@ -312,7 +317,7 @@ protected function orderedLazyById(int $chunkSize = 1000, ?string $column = null
return;
}

$lastId = $results->last()->{$alias};
$lastId = data_get($results->last(), $alias);

if ($lastId === null) {
throw new RuntimeException("The lazyById operation was aborted because the [{$alias}] column is not present in the query result.");
Expand Down Expand Up @@ -341,8 +346,11 @@ public function first(array|string $columns = ['*'])
*/
public function firstOrFail(array|string $columns = ['*'], ?string $message = null)
{
if (! is_null($result = $this->first($columns))) {
return $result;
// Inspect collection presence so a matching scalar null row is not mistaken for no row.
$results = $this->limit(1)->get($columns);

if ($results->isNotEmpty()) {
return $results->first();
}

throw new RecordNotFoundException($message ?: 'No record found for the given query.');
Expand Down
36 changes: 28 additions & 8 deletions src/database/src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -408,11 +408,11 @@ public function select(string $query, array $bindings = [], bool $useReadPdo = t
}

/**
* Run a select statement against the database and returns all of the result sets.
* Run a select statement against the database and return all of the result sets.
*/
public function selectResultSets(string $query, array $bindings = [], bool $useReadPdo = true): array
public function selectResultSets(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []): array
{
return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo, $fetchUsing) {
if ($this->pretending()) {
return [];
}
Expand All @@ -428,23 +428,23 @@ public function selectResultSets(string $query, array $bindings = [], bool $useR
$sets = [];

do {
$sets[] = $statement->fetchAll();
$sets[] = $statement->fetchAll(...$fetchUsing);
} while ($statement->nextRowset());

return $sets;
});
}

/**
* Run a select statement against the database and returns a generator.
* Run a select statement against the database and return a generator.
*
* @return Generator<int, stdClass>
* @return Generator<int, mixed>
*/
public function cursor(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []): Generator
{
$statement = $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
if ($this->pretending()) {
return [];
return null;
}

// First we will create a statement for the query. Then, we will set the fetch
Expand All @@ -466,7 +466,27 @@ public function cursor(string $query, array $bindings = [], bool $useReadPdo = t
return $statement;
});

while ($record = $statement->fetch(...$fetchUsing)) {
if ($statement === null) {
return;
}

if ($fetchUsing !== []) {
// fetchAll() supplies default column and class arguments that setFetchMode()
// demands explicitly, so a mode-only call keeps the same meaning when streamed.
if (count($fetchUsing) === 1) {
$mode = $fetchUsing[0] & ~(PDO::FETCH_GROUP | PDO::FETCH_UNIQUE | PDO::FETCH_CLASSTYPE | PDO::FETCH_PROPS_LATE);

if ($mode === PDO::FETCH_COLUMN) {
$fetchUsing[] = 0;
} elseif ($mode === PDO::FETCH_CLASS && ($fetchUsing[0] & PDO::FETCH_CLASSTYPE) === 0) {
$fetchUsing[] = stdClass::class;
}
}

$statement->setFetchMode(...$fetchUsing);
}

foreach ($statement as $record) {
yield $record;
}
}
Expand Down
11 changes: 10 additions & 1 deletion src/database/src/ConnectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ public function scalar(string $query, array $bindings = [], bool $useReadPdo = t
public function select(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []): array;

/**
* Run a select statement against the database and returns a generator.
* Run a select statement against the database and return a generator.
*
* @return Generator<int, mixed>
*/
public function cursor(string $query, array $bindings = [], bool $useReadPdo = true, array $fetchUsing = []): Generator;

Expand Down Expand Up @@ -113,6 +115,8 @@ public function rollBack(?int $toLevel = null): void;

/**
* Get the number of active transactions.
*
* @phpstan-impure
*/
public function transactionLevel(): int;

Expand Down Expand Up @@ -148,6 +152,11 @@ public function threadCount(): ?int;

/**
* Run a callback without the table prefix on the connection.
*
* @template TReturn
*
* @param Closure($this): TReturn $callback
* @return TReturn
*/
public function withoutTablePrefix(Closure $callback): mixed;

Expand Down
3 changes: 2 additions & 1 deletion src/database/src/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@
*
* @method $this whereCan(\UnitEnum|string $ability, mixed $user = null)
* @method $this withCan(\UnitEnum|string|list<\UnitEnum|string> $abilities, mixed $user = null)
* @method $this fetchUsing(mixed ...$fetchUsing)
*
* @mixin \Hypervel\Database\Query\Builder
*/
class Builder implements BuilderContract
{
/** @use \Hypervel\Database\Concerns\BuildsQueries<TModel> */
/** @use \Hypervel\Database\Concerns\BuildsQueries<int, TModel> */
use BuildsQueries, ForwardsCalls, QueriesRelationships {
BuildsQueries::sole as baseSole;
}
Expand Down
Loading
Loading