Skip to content

Commit 1993c2e

Browse files
committed
added AGENTS.md & DOCS
1 parent 0dd92a4 commit 1993c2e

8 files changed

Lines changed: 540 additions & 0 deletions

File tree

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
.gitattributes export-ignore
22
.github/ export-ignore
33
.gitignore export-ignore
4+
AGENTS.md export-ignore
45
ncs.* export-ignore
56
phpstan*.neon export-ignore
67
src/**/*.latte export-ignore
8+
docs/ export-ignore
79
tests/ export-ignore
810

911
*.php* diff=php

AGENTS.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# To My Agents!
2+
3+
It is my fervent wish that this file guide every AI coding agent working with code in this repository.
4+
5+
## Documentation
6+
7+
Any distilled, agent-facing documentation for this package - how it works
8+
internally and the rationale behind key design decisions - lives in `docs/`.
9+
Consult it before non-trivial changes; it is the source of truth from which the
10+
public manual is distilled.
11+
12+
Two independent worlds - the low-level core and the Explorer (ActiveRow) layer -
13+
each with sharp edges (lazy execution, accessed-column narrowing, N+1 batching,
14+
context-detected preprocessor modes). Read `docs/internals/` before touching them.
15+
16+
## Project Overview
17+
18+
**Nette Database** is a database abstraction layer offering two components:
19+
20+
1. **Core** - a PDO wrapper with an advanced SQL preprocessor and parameter
21+
substitution.
22+
2. **Explorer** - an ActiveRow layer (inspired by NotORM) with convention-based
23+
relationships and automatic N+1 prevention.
24+
25+
Supports MySQL, PostgreSQL, SQLite, MS SQL Server, and Oracle.
26+
27+
- **PHP Version**: 8.1 - 8.5
28+
- **Package**: `nette/database`
29+
30+
## Essential Commands
31+
32+
```bash
33+
# Run all tests
34+
vendor/bin/tester tests -s -C
35+
36+
# Run one test directory / file
37+
vendor/bin/tester tests/Database/Explorer -s -C
38+
vendor/bin/tester tests/Database/Explorer/Explorer.basic.phpt -s -C
39+
40+
# Static analysis (PHPStan level 5 + phpstan-nette)
41+
composer phpstan
42+
```
43+
44+
Most tests connect to real MySQL/PostgreSQL/MS SQL servers via
45+
`@dataProvider databases.ini`. Bring the servers up with the repo's
46+
`docker-compose.yml` (`docker compose up -d`, wait for `healthy`) before running
47+
them; a `Connection refused` / `could not find driver` failure means the servers
48+
aren't up yet, not a broken test.
49+
50+
## Conventions
51+
52+
- Every file starts with `declare(strict_types=1);`; everything typed; single
53+
quotes unless the string contains an apostrophe; Nette Coding Standard.
54+
- Use generic annotations for IDE/PHPStan support: `@return Selection<ProductRow>`,
55+
`@template T of ActiveRow`. Method phpDoc starts with a 3rd-person verb (Returns,
56+
Formats, Checks); document a param/return only when it adds info beyond the type.
57+
- Tests are Nette Tester `.phpt` files; use `@dataProvider databases.ini` to run
58+
against every engine, `test()` / `testException()` with descriptive names, and
59+
**no comment before `test()`**. Fixtures: `tests/Database/files/{driver}-nette_test1.sql`.
60+
61+
## Working in this repo
62+
63+
- **The Explorer is lazy and self-narrowing.** `accessColumn` is the single seam
64+
every read passes through; a first query fetches `SELECT *`, later ones narrow to
65+
the accessed columns (cached), and relations are batched to avoid N+1. The cache
66+
key even depends on the call-site (`debug_backtrace`) - a real refactor trap. See
67+
`docs/internals/explorer.md`.
68+
- **The SQL preprocessor picks its array mode from surrounding SQL context**
69+
(`?and`/`?set`/`?values`/`?order`/`?list`), so the same array expands differently
70+
after `WHERE` vs `SET` vs `INSERT`. See `docs/internals/sql-preprocessor.md`.
71+
- **Nested transactions use a depth counter, not savepoints** - only the outermost
72+
`transaction()` issues a real BEGIN/COMMIT/ROLLBACK; there is no partial rollback,
73+
and no retry mechanism (no `$attempts`, no `RetryableException`, no `onRetry`).
74+
There is no `TypeConverter` class either (DB->PHP conversion is
75+
`Helpers::normalizeRow`). Don't document designed-but-absent features as present.
76+
- **Array expansion is a mass-assignment surface.** Passing raw user input as the
77+
array to `insert`/`update`/`where` lets an attacker set arbitrary columns and
78+
inject operators/SQL via keys - always whitelist columns first. Full guidance is
79+
web-manual material.
80+
- User-facing how-to (Explorer/Selection API, `?`-placeholder reference, NEON
81+
config, transactions, Reflection API) is manual material and lives in the public
82+
web docs, not here.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Connection & drivers
2+
3+
## Execution path
4+
5+
`Connection` connects **lazily** — the constructor opens the PDO only when the `lazy`
6+
option is falsy; otherwise the first `getPdo()`/`getDriver()`/`preprocess()` triggers
7+
`connect()` (which also instantiates the driver from `PDO::ATTR_DRIVER_NAME`, builds
8+
the `SqlPreprocessor`, and fires `onConnect`).
9+
10+
`query()` = `preprocess()` (which runs the preprocessor only when there are
11+
parameters) → `new ResultSet(...)`. **The SQL actually executes in the `ResultSet`
12+
constructor, not in `query()`** — so timing, binding, and exception conversion all
13+
live there (see results-and-types.md). The `fetch*` shortcuts on `Connection` just
14+
delegate to `query(...)->fetch*()`.
15+
16+
## The `Driver` dialect abstraction
17+
18+
Each engine implements `Driver`: `delimite` (identifier quoting — MySQL backticks,
19+
PgSql double-quotes, both doubling), `formatDateTime`/`formatDateInterval`/`formatLike`,
20+
`applyLimit`, schema reflection (`getTables`/`getColumns`/`getIndexes`/`getForeignKeys`/
21+
`getColumnTypes`), `convertException`, and `isSupported` over the `Support*` feature
22+
constants.
23+
24+
**`applyLimit` is the most dialect-divergent piece** — MySQL uses `LIMIT` (with the
25+
`LIMIT 18446744073709551615 OFFSET` trick for offset-only), PgSql separate `LIMIT`/
26+
`OFFSET`, SQL Server `OFFSET … ROWS FETCH NEXT … ROWS ONLY`, MS SQL/ODBC inject a
27+
`TOP n` (no offset), Oracle wraps in a `ROWNUM` subquery. Result-set type detection is
28+
per-driver `getColumnTypes`: PgSql and MsSql map the whole result set via
29+
`Helpers::detectTypes`, MySQL/SQLite/Sqlsrv go column-by-column via
30+
`Helpers::detectType`, and Odbc/Oci detect nothing. MySQL adds dialect rules
31+
(`NEWDECIMAL` precision 0 → integer, `TINY` len 1 + `convertBoolean` → bool, `TIME`
32+
interval).
33+
34+
## Exception mapping
35+
36+
```
37+
\PDOException → DriverException
38+
├── ConnectionException → ConnectionLostException
39+
├── ConstraintViolationException
40+
│ ├── ForeignKey / NotNull / Unique / CheckConstraintViolation
41+
├── DeadlockException
42+
└── LockTimeoutException
43+
```
44+
45+
Note `Deadlock`/`LockTimeout` extend `DriverException` **directly**, not the
46+
constraint hierarchy. There is **no** `RetryableException` marker and `transaction()`
47+
performs no retries — retrying on deadlock/lock-timeout/connection-lost is the
48+
caller's job. The mapping is **per driver** in
49+
`convertException`: MySQL keys on the numeric error code, PgSql on the SQLSTATE; an
50+
unrecognized error falls back to a bare `DriverException::from()`. `DriverException::from`
51+
parses `errorInfo`, or the `SQLSTATE[..] [..] ..` pattern from the message when
52+
`errorInfo` is absent. Conversion is invoked in the `ResultSet` constructor (which also
53+
attaches the query string and params) and in `getInsertId`; `connect()`/`quote()` use
54+
`ConnectionException::from`/`DriverException::from` directly because the driver may not
55+
exist yet.

0 commit comments

Comments
 (0)