Invoke before writing database queries or working with Selection API, ActiveRow in Nette.
/plugin marketplace add nette/claude-code/plugin install nette@netteThis skill inherits all available tools. When active, it can use any tool Claude has access to.
explorer.mdsql-way.mdUses Nette Database with MySQL 8.4+ as the backend.
composer require nette/database
For complete Explorer API, see explorer.md. For SQL queries, see sql-way.md.
user not users)id for primary keysutf8mb4_cs_0900_ai_ci for Czech-language applicationsutf8mb4_0900_ai_ci for English-language applicationscreated_at DATETIME DEFAULT CURRENT_TIMESTAMPExtends Nette Database Explorer to automatically map database tables to typed entity classes.
Core benefit: Zero-configuration entity mapping with full IDE support. How it works: Converts table names (snake_case) to entity class names (PascalCase + Row suffix).
All entities in App\Entity with consistent Row suffix:
product table → ProductRoworder_item table → OrderItemRowvariant_expiration table → VariantExpirationRowWhy flat: Entities are data structures that cross domain boundaries. ProductRow used in catalog, orders, inventory, and reporting contexts.
All entities in single App\Entity namespace - avoid domain subdivision:
app/Entity/
├── ProductRow.php ← Core business entities
├── OrderItemRow.php ← Relationship entities
└── StockTransferRow.php ← Operational entities
/**
* @property-read int $id
* @property-read string $title
* @property-read bool $active
* @property-read ?CategoryRow $category ← nullable relationship
* @property-read UserRow $author ← required relationship
*/
final class ProductRow extends Table\ActiveRow
{
}
Documentation rules:
Foreign key patterns:
@property-read ?CategoryRow $category for optional relationships@property-read UserRow $author for required relationships@property-read Selection<OrderItemRow> $order_items for back-referencesNaming convention: Follow Nette Database relationship naming (foreign key without _id suffix).
Use for:
return $this->db->table('product')
->where('active', true)
->where('category_id', $categoryId)
->order('name');
Use for:
return $this->db->query('
WITH RECURSIVE category_tree AS (...)
SELECT ...
', $params)->fetchAll();
Progressive refinement - start with base methods, refine with conditions:
Always use generic types for Selection returns:
/** @return Selection<ProductRow> */
public function getProducts(): Selection
{
return $this->db->table('product');
}
/** @return Selection<ProductRow> */
public function getActiveProducts(): Selection
{
return $this->getProducts()->where('active', true);
}
/** @return Selection<ProductRow> */
public function getProductsInCategory(int $categoryId): Selection
{
return $this->getActiveProducts()
->where(':product_category.category_id', $categoryId);
}
Benefits: Reusable base queries, clear evolution of filtering logic, easy testing. Benefits: Full IDE support, type safety, clear contracts.
Use colon notation for efficient joins:
// Forward relationship (via foreign key)
->where('category.slug', $categorySlug)
// Back-reference (reverse relationship)
->where(':order_item.quantity >', 1)
// Deep relationships
->where('category.parent.name', 'Root Category')
Single optional result: ->fetch()
All results as array: ->fetchAll()
Key-value pairs: ->fetchPairs('key_column', 'value_column')
Structured data: ->fetchAssoc('key_column->')
Count only: ->count('*')
Use direct SQL migrations rather than ORM-style migrations:
sql/db.sqlRely on database constraints for data integrity:
Handle constraint violations in services with meaningful business exceptions.
Don't create separate Repository classes - combine data access with business logic in services. Don't use ActiveRecord for complex queries - raw SQL is cleaner for analytics and reporting. Don't fetch more data than needed - use appropriate fetching methods and SELECT only required columns for large datasets.
Transform database errors to business exceptions:
try {
$customer->update(['email' => $newEmail]);
} catch (Nette\Database\UniqueConstraintViolationException) {
throw new EmailAlreadyExistsException();
}
Handle at service boundary - presenters should receive business exceptions, not database exceptions.
database:
dsn: 'mysql:host=127.0.0.1;dbname=myapp'
user: root
password: secret
options:
lazy: true # Connect on first query
charset: utf8mb4 # Default
convertBoolean: true # TINYINT(1) to bool
newDateTime: true # Return DateTimeImmutable
Multiple connections:
database:
main:
dsn: 'mysql:host=127.0.0.1;dbname=app'
user: root
password: secret
logs:
dsn: 'mysql:host=127.0.0.1;dbname=logs'
user: logs
password: secret
autowired: false # Must reference explicitly
Reference non-autowired connection:
services:
- LogService(@database.logs.connection)
For detailed information, fetch from doc.nette.org:
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.