Skip to content

Commit 9aaef71

Browse files
chore: expand query builder contract, schema resolver, and test coverage
1 parent e6bef1a commit 9aaef71

34 files changed

Lines changed: 1661 additions & 301 deletions

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,29 @@
22

33
All significant changes to this project will be documented in this file.
44

5+
## [2.0.0] - 2025-12-22
6+
7+
### Added
8+
9+
- **Schema:** `GrammarResolver` and `Schema::forConnection()` to centralize grammar selection.
10+
- **QueryBuilder:** configurable empty `whereIn`/`whereNotIn` behavior.
11+
- **Docs/typing:** richer PHPDoc generics for ModelQuery/relations and query builder return shapes.
12+
13+
### Changed
14+
15+
- **QueryBuilder:** improved alias handling for `select/value/pluck` and `table.*` wrapping.
16+
- **Schema:** SQLite migration table DDL now uses SQLite-specific syntax.
17+
18+
### Fixed
19+
20+
- **SoftDeletes:** unsaved models no longer insert on delete.
21+
- **ModelQuery:** methods that return values now return the builder result (not always `$this`).
22+
- **Schema:** unified MySQL grammar to a single canonical class.
23+
24+
### Tests
25+
26+
- Expanded coverage across QueryBuilder, Schema/Grammar, migrations, and transactions.
27+
528
## [1.6.0] - 2025-12-22
629

730
### Added

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ $users = $db->table('users')
8181
->get();
8282
```
8383

84+
#### SELECT with aliases
85+
86+
```php
87+
$rows = $db->table('users')
88+
->select('users.name label', 'COUNT(*) total')
89+
->groupBy('users.name')
90+
->get();
91+
```
92+
8493
#### INSERT
8594

8695
```php
@@ -183,6 +192,19 @@ $email = $db->table('users')
183192

184193
$names = $db->table('users')->pluck('name');
185194
$pairs = $db->table('users')->pluck('email', 'id'); // [id => email]
195+
196+
// Aliases are supported:
197+
// $db->table('users')->pluck('users.name label', 'users.id key');
198+
// $db->table('users')->value('COUNT(*) total');
199+
```
200+
201+
#### Value / Pluck with aliases
202+
203+
```php
204+
$pairs = $db->table('users')
205+
->pluck('users.name label', 'users.id key'); // [id => name]
206+
207+
$total = $db->table('users')->value('COUNT(*) total');
186208
```
187209

188210
#### Pagination
@@ -201,6 +223,28 @@ $page = $db->table('posts')->simplePaginate(20, $currentPage);
201223
// ];
202224
```
203225

226+
#### Empty whereIn / whereNotIn
227+
228+
```php
229+
$db->table('users')
230+
->setEmptyWhereInBehavior(\Codemonster\Database\Query\QueryBuilder::EMPTY_CONDITION_EXCEPTION)
231+
->whereIn('id', []);
232+
```
233+
234+
Available behaviors:
235+
236+
- `EMPTY_CONDITION_NONE` (default for `whereIn`) -> executes `0 = 1`
237+
- `EMPTY_CONDITION_ALL` (default for `whereNotIn`) -> executes `1 = 1`
238+
- `EMPTY_CONDITION_EXCEPTION` -> throws `InvalidArgumentException`
239+
240+
You can override `whereNotIn` separately:
241+
242+
```php
243+
$db->table('users')
244+
->setEmptyWhereNotInBehavior(\Codemonster\Database\Query\QueryBuilder::EMPTY_CONDITION_NONE)
245+
->whereNotIn('id', []);
246+
```
247+
204248
### 3. Transactions
205249

206250
```php
@@ -244,6 +288,7 @@ The package includes a lightweight schema builder.
244288
```php
245289
use Codemonster\Database\Schema\Blueprint;
246290

291+
// You can also use Schema::forConnection($db) if you need a schema instance directly.
247292
$db->schema()->create('users', function (Blueprint $table) {
248293
$table->id();
249294
$table->string('name');
@@ -262,6 +307,11 @@ $db->schema()->table('users', function (Blueprint $table) {
262307
});
263308
```
264309

310+
### SQLite notes
311+
312+
- SQLite supports `ALTER TABLE` only for a subset of operations; some drop operations are ignored.
313+
- Foreign keys are emitted inline during `CREATE TABLE`.
314+
265315
### Dropping a table
266316

267317
```php

src/Connection.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use Codemonster\Database\Contracts\QueryBuilderInterface;
77
use Codemonster\Database\Exceptions\QueryException;
88
use Codemonster\Database\Query\QueryBuilder;
9-
use Codemonster\Database\Schema\MySqlGrammar;
109
use Codemonster\Database\Schema\Schema;
1110
use PDO;
1211
use PDOException;
@@ -25,7 +24,7 @@ public function __construct(array $config)
2524
{
2625
$driver = $config['driver'] ?? 'mysql';
2726

28-
return match ($driver) {
27+
match ($driver) {
2928
'mysql' => $this->connectMySql($config),
3029
'sqlite' => $this->connectSqlite($config),
3130
default => throw new InvalidArgumentException("Unsupported driver [$driver].")
@@ -166,6 +165,11 @@ public function rollBack(): bool
166165
return $this->pdo->rollBack();
167166
}
168167

168+
/**
169+
* @template T
170+
* @param callable(self):T $callback
171+
* @return T
172+
*/
169173
public function transaction(callable $callback): mixed
170174
{
171175
$this->beginTransaction();
@@ -191,6 +195,6 @@ public function transaction(callable $callback): mixed
191195

192196
public function schema(): Schema
193197
{
194-
return new Schema($this, new MySqlGrammar());
198+
return Schema::forConnection($this);
195199
}
196200
}

src/Contracts/ConnectionInterface.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public function delete(string $query, array $params = []): int;
2323

2424
public function statement(string $query, array $params = []): bool;
2525

26+
/**
27+
* @return \Codemonster\Database\Contracts\QueryBuilderInterface
28+
*/
2629
public function table(string $table): QueryBuilderInterface;
2730

2831
public function beginTransaction(): bool;

src/Contracts/QueryBuilderInterface.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,56 @@
44

55
interface QueryBuilderInterface
66
{
7+
public function select(string|array ...$columns): static;
8+
public function selectRaw(string $expression): static;
9+
public function distinct(): static;
10+
public function where(string|callable $column, mixed $operator = null, mixed $value = null, string $boolean = 'AND'): static;
11+
public function orWhere(string|callable $column, mixed $operator = null, mixed $value = null): static;
12+
public function whereIn(string $column, array $values, string $boolean = 'AND'): static;
13+
public function orWhereIn(string $column, array $values): static;
14+
public function whereNotIn(string $column, array $values, string $boolean = 'AND'): static;
15+
public function orWhereNotIn(string $column, array $values): static;
16+
public function setEmptyWhereInBehavior(string $behavior): static;
17+
public function setEmptyWhereNotInBehavior(string $behavior): static;
18+
public function whereNull(string $column, string $boolean = 'AND'): static;
19+
public function orWhereNull(string $column): static;
20+
public function whereNotNull(string $column, string $boolean = 'AND'): static;
21+
public function orWhereNotNull(string $column): static;
22+
public function whereBetween(string $column, array $range, string $boolean = 'AND'): static;
23+
public function orWhereBetween(string $column, array $range): static;
24+
public function whereNotBetween(string $column, array $range, string $boolean = 'AND'): static;
25+
public function orWhereNotBetween(string $column, array $range): static;
26+
public function whereRaw(string $expression, array $bindings = [], string $boolean = 'AND'): static;
27+
public function orWhereRaw(string $expression, array $bindings = []): static;
28+
public function join(string $table, string|callable $first, ?string $operator = null, ?string $second = null, string $type = 'INNER'): static;
29+
public function leftJoin(string $table, string|callable $first, ?string $operator = null, ?string $second = null): static;
30+
public function rightJoin(string $table, string|callable $first, ?string $operator = null, ?string $second = null): static;
31+
public function crossJoin(string $table): static;
32+
public function groupBy(string|array $columns): static;
33+
public function having(string $column, string $operator, mixed $value, string $boolean = 'AND'): static;
34+
public function orHaving(string $column, string $operator, mixed $value): static;
35+
public function havingRaw(string $expression, string $boolean = 'AND'): static;
36+
public function orderBy(string $column, string $direction = 'asc'): static;
37+
public function orderByRaw(string $expression): static;
38+
public function limit(int $value): static;
39+
public function offset(int $value): static;
740
public function get(): array;
841
public function first(): ?array;
942
public function toSql(): string;
43+
public function getBindings(): array;
44+
public function insert(array $values): bool;
45+
public function insertGetId(array $values): int;
46+
public function update(array $values): int;
47+
public function delete(): int;
48+
public function count(string $column = '*'): int;
49+
public function sum(string $column): float|int;
50+
public function avg(string $column): float|int;
51+
public function min(string $column): float|int;
52+
public function max(string $column): float|int;
53+
public function exists(): bool;
54+
public function doesntExist(): bool;
55+
public function value(string $column): mixed;
56+
public function pluck(string $column, ?string $key = null): array;
57+
public function forPage(int $page, int $perPage): static;
58+
public function simplePaginate(int $perPage = 15, int $page = 1): array;
1059
}

src/Migrations/MigrationRepository.php

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Codemonster\Database\Migrations;
44

55
use Codemonster\Database\Contracts\ConnectionInterface;
6+
use PDO;
67

78
class MigrationRepository
89
{
@@ -20,18 +21,39 @@ public function __construct(ConnectionInterface $connection, string $table = 'mi
2021

2122
public function ensureTableExists(): void
2223
{
23-
$sql = <<<SQL
24-
CREATE TABLE IF NOT EXISTS `{$this->table}` (
25-
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
26-
`migration` VARCHAR(255) NOT NULL,
27-
`batch` INT NOT NULL,
28-
PRIMARY KEY (`id`)
29-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
30-
SQL;
24+
$driver = $this->getDriverName();
25+
26+
if ($driver === 'sqlite') {
27+
$sql = <<<SQL
28+
CREATE TABLE IF NOT EXISTS "{$this->table}" (
29+
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
30+
"migration" TEXT NOT NULL,
31+
"batch" INTEGER NOT NULL
32+
);
33+
SQL;
34+
} else {
35+
$sql = <<<SQL
36+
CREATE TABLE IF NOT EXISTS `{$this->table}` (
37+
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
38+
`migration` VARCHAR(255) NOT NULL,
39+
`batch` INT NOT NULL,
40+
PRIMARY KEY (`id`)
41+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
42+
SQL;
43+
}
3144

3245
$this->connection->statement($sql);
3346
}
3447

48+
protected function getDriverName(): ?string
49+
{
50+
try {
51+
return $this->connection->getPdo()->getAttribute(PDO::ATTR_DRIVER_NAME);
52+
} catch (\Throwable) {
53+
return null;
54+
}
55+
}
56+
3557
/**
3658
* Get all ran migrations ordered by batch and id.
3759
*

src/ORM/Model.php

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ protected static function connection(): ConnectionInterface
6666
return call_user_func(static::$connectionResolver, static::class);
6767
}
6868

69+
/**
70+
* @return ModelQuery<static>
71+
*/
6972
public static function query(): ModelQuery
7073
{
7174
$instance = new static();
@@ -76,6 +79,9 @@ public static function query(): ModelQuery
7679
return new ModelQuery($builder, static::class);
7780
}
7881

82+
/**
83+
* @return ModelCollection<static>
84+
*/
7985
public static function all(): ModelCollection
8086
{
8187
return static::query()->get();
@@ -101,6 +107,8 @@ public static function create(array $attributes): static
101107

102108
/**
103109
* Hydration of an array of strings into a collection of models.
110+
*
111+
* @return ModelCollection<static>
104112
*/
105113
public static function hydrate(array $rows): ModelCollection
106114
{
@@ -162,16 +170,16 @@ public function save(): bool
162170

163171
public function delete(): bool
164172
{
173+
if (!$this->exists) {
174+
return false;
175+
}
176+
165177
// support for soft deletes (via trait)
166178
if (method_exists($this, 'runSoftDelete')) {
167179
/** @var callable $m */
168180
return $this->runSoftDelete();
169181
}
170182

171-
if (!$this->exists) {
172-
return false;
173-
}
174-
175183
/** @var QueryBuilder $builder */
176184
$builder = static::connection()->table($this->getTable());
177185

@@ -365,6 +373,11 @@ protected function getRelationshipFromMethod(string $method)
365373
return $results;
366374
}
367375

376+
/**
377+
* @template TRelated of Model
378+
* @param class-string<TRelated> $related
379+
* @return HasOne<TRelated, static>
380+
*/
368381
public function hasOne(string $related, ?string $foreignKey = null, ?string $localKey = null): HasOne
369382
{
370383
/** @var Model $instance */
@@ -382,6 +395,11 @@ public function hasOne(string $related, ?string $foreignKey = null, ?string $loc
382395
);
383396
}
384397

398+
/**
399+
* @template TRelated of Model
400+
* @param class-string<TRelated> $related
401+
* @return HasMany<TRelated, static>
402+
*/
385403
public function hasMany(string $related, ?string $foreignKey = null, ?string $localKey = null): HasMany
386404
{
387405
/** @var Model $instance */
@@ -399,6 +417,11 @@ public function hasMany(string $related, ?string $foreignKey = null, ?string $lo
399417
);
400418
}
401419

420+
/**
421+
* @template TRelated of Model
422+
* @param class-string<TRelated> $related
423+
* @return BelongsTo<TRelated, static>
424+
*/
402425
public function belongsTo(string $related, ?string $foreignKey = null, ?string $ownerKey = null): BelongsTo
403426
{
404427
/** @var Model $instance */
@@ -417,6 +440,11 @@ public function belongsTo(string $related, ?string $foreignKey = null, ?string $
417440
);
418441
}
419442

443+
/**
444+
* @template TRelated of Model
445+
* @param class-string<TRelated> $related
446+
* @return BelongsToMany<TRelated, static>
447+
*/
420448
public function belongsToMany(
421449
string $related,
422450
?string $pivotTable = null,

0 commit comments

Comments
 (0)