# AutoSlave Drupal Module - AI Coding Instructions

## Project Overview

AutoSlave is a Drupal 8/9/10 module implementing MySQL master/slave database replication with automatic read/write query routing, failover support, and replication lag mitigation.

**Core Architecture**: Custom database driver that wraps standard Drupal database connections, intercepting queries to intelligently route reads to slaves and writes to masters while tracking affected tables for consistency.

## Project Structure

```
autoslave/
├── docs/                           # Documentation files
│   ├── ARCHITECTURAL_UPGRADE_PLAN.md  # Comprehensive upgrade strategy
│   ├── BREAKING_CHANGES.md           # Migration guide for refactoring
│   ├── FIXES_APPLIED.md              # Log of fixes applied
│   └── UPGRADE.md                    # Drupal 10 upgrade guide
├── scripts/                        # Refactoring automation scripts
│   ├── refactor-all.sh              # Master refactoring script
│   ├── fix-constants.sh             # Fix TRUE/FALSE/NULL casing
│   ├── refactor-properties.sh       # Rename properties to lowerCamel
│   └── refactor-methods.sh          # Rename methods to lowerCamel
├── lib/                            # Database driver implementation
├── src/                            # Controllers, Forms, Services
├── config/                         # Configuration schemas
├── templates/                      # Twig templates
└── [module files]                  # .module, .install, .info.yml, etc.
```

**Important:** Refactoring scripts in `scripts/` directory automate the migration to Drupal coding standards. See `docs/BREAKING_CHANGES.md` for details.

## Critical Setup Requirements

### Non-Standard Module Structure

This module has a **dual-location architecture** that is unusual for Drupal modules:

1. **Standard module files**: `modules/autoslave/` (typical Drupal structure)
2. **Database driver files**: Must be **manually copied** to `core/lib/Drupal/Core/Database/Driver/autoslave/`

The database driver lives in `lib/Drupal/autoslave/Database/Driver/autoslave/` but **MUST** be deployed to the core directory for Drupal to recognize it. This is not handled by Drupal's module system.

### Installation Command (Critical)

```bash
cp -r modules/autoslave/lib/Drupal/autoslave/Database/Driver/autoslave core/lib/Drupal/Core/Database/Driver/
```

## Database Driver Implementation

### Namespace Quirk (Critical Understanding)

The driver files use `namespace Drupal\Core\Database\Driver\autoslave` (in **core** namespace) even though source files live in the module. This is because:

- Drupal's database layer expects drivers in the `Drupal\Core\Database\Driver\` namespace
- The files are physically copied to `core/lib/` during installation
- This enables the `'driver' => 'autoslave'` configuration to work in `settings.php`

### Driver Class Architecture

**Connection.php** (main orchestrator):

- Extends `Drupal\Core\Database\Connection`
- Maintains connection pools (`$__pool`) with weighted random selection
- Implements `forceMaster()` counter for temporary master-only mode
- Tracks affected tables (`$__tables`) to route subsequent reads to master
- Handles failover by invalidating failed connections and re-selecting

**Query Classes** (Merge, Select, Insert, Update, Delete, Truncate, Upsert):

- Minimal stubs extending core query classes
- Routing logic handled in `Connection::query()`, not in query objects
- Pattern: `class ClassName extends QueryClassName {}`

### Master/Slave Routing Logic

**Write Operations**: Always routed to master via `determineMasterTarget()`

- INSERT, UPDATE, DELETE, MERGE, TRUNCATE, CREATE, ALTER, DROP
- Tables written to are automatically added to `$__tables` via `addAffectedTable()`

**Read Operations**: Routed based on table state

- Goes to **master** if table is in `$__tables` (recently written within replication lag window)
- Goes to **master** if `forceMaster()` counter > 0 (lock acquisition)
- Goes to **slave** otherwise

**Affected Tables Tracking**:

- Session-based (`$_SESSION['autoslave_affected_tables']`) or database-based
- Expires after configured replication lag (default 2 seconds)
- Backend configured via `'affected tables backend'` setting

## Settings.php Configuration Patterns

### Minimal Setup (1 master, 1 slave):

```php
$databases['default']['default'] = [
  'driver' => 'autoslave',
  'master' => 'master',  // target name
  'slave' => 'autoslave', // target name
  'tables' => ['sessions', 'semaphore'], // always use master
];

$databases['default']['master'] = [...]; // actual MySQL connection
$databases['default']['autoslave'] = [...]; // actual MySQL connection
```

### Multi-Server with Failover:

```php
$databases['default']['default'] = [
  'driver' => 'autoslave',
  'master' => ['master', 'slave1', 'slave2'], // fallback order
  'slave' => ['slave1', 'slave2', 'master'],  // fallback order
  'replication lag' => 2,
  'global replication lag' => TRUE,
  'invalidation path' => 'sites/default/files',
];

$databases['default']['master'][] = [...]; // can be array of connections
$databases['default']['slave1'][] = ['readonly' => TRUE, 'weight' => 70, ...];
```

**Key Options**:

- `'replication lag'`: Seconds to track affected tables (default: 2)
- `'global replication lag'`: Use DB-based tracking vs session-only (default: TRUE)
- `'invalidation path'`: File-based connection failure tracking
- `'weight'`: Connection selection probability (default: 100)
- `'readonly'`: Mark slave-only connections for failover scenarios

### CLI/Drush Workaround (Required)

Drush doesn't support non-PDO drivers, so force direct master connection:

```php
if (drupal_is_cli() || basename($_SERVER['PHP_SELF']) == 'update.php') {
  $databases['default']['default'] = $databases['default']['master'];
}
```

## Lock Backend Integration

**AutoSlaveLockBackend** (`src/Lock/AutoSlaveLockBackend.php`):

- Wraps another lock backend (typically `DatabaseLockBackend`)
- Increments `forceMaster()` counter when lock acquired
- Decrements counter when lock released
- **Why**: Locks imply writes are coming, ensure consistency by routing to master

Configuration:

```php
$conf['lock_class'] = 'Drupal\autoslave\Lock\AutoSlaveLockBackend';
$conf['autoslave_lock_class'] = 'Drupal\Core\Lock\DatabaseLockBackend';
```

## Key Files and Their Roles

- `lib/Drupal/autoslave/Database/Driver/autoslave/Connection.php` - Core routing logic, connection pooling, failover
- `lib/Drupal/autoslave/Database/Driver/autoslave/defines.inc` - Version constants and defaults
- `autoslave.module` - Hooks for batch operations (SimpleTest compatibility), query tag alters
- `src/Lock/AutoSlaveLockBackend.php` - Lock system integration for master forcing
- `src/Controller/AutoslaveController.php` - Admin pages for status and affected tables
- `autoslave.install` - Schema for `autoslave_affected_tables` tracking table

## Testing and Debugging

### Check Driver Status

```php
autoslave_is_driver_loaded(); // Returns TRUE if active
Database::getConnection('default', 'default')->driver(); // Returns 'autoslave'
```

### Admin Pages

- `/admin/config/system/autoslave` - Settings form
- `/admin/config/system/autoslave/status` - Connection pool status
- `/admin/config/system/autoslave/affected-tables` - Real-time table tracking

### Debugging Tips

- Set `'debug' => TRUE` in connection options
- Check `autoslave_affected_tables` table for tracking data
- Look for invalidation files in configured path: `autoslave-invalidation-{key}.inc`
- Watchdog logging controlled by `'watchdog on shutdown'` option

## Common Gotchas

1. **SimpleTest/PHPUnit**: Batch alter hook forces hard switch to master for test operations
2. **Transactions**: All queries within a transaction go to master (tracked via `transactionDepth()`)
3. **Replication Lag**: Increase `'replication lag'` if seeing stale reads after writes
4. **Shutdown Functions**: Database watchdog may fail during shutdown if master unavailable
5. **Read-Only Mode**: System enters this mode if all masters are down and only read-only slaves available

## Development Patterns

### Explicitly Route to Slave

```php
$query = $db->select('node', 'n', ['target' => 'slave']);
```

### Force Master for Critical Operations

```php
$connection->forceMaster(1); // Increment counter
// ... perform operations ...
$connection->forceMaster(-1); // Decrement counter
```

### Check Connection Pool

```php
$pool = $connection->getPool();
// Returns: ['master' => [...], 'slave' => [...], 'all' => [...]]
```

## Module-Specific Conventions

- **No composer dependencies**: Pure Drupal 8+ compatible code
- **Version tracking**: `AUTOSLAVE_VERSION` constant for session backward compatibility
- **Affected tables backends**: Pluggable via `'affected tables backend'` (file includes)
- **Weighted random selection**: `rand_weighted()` method for multi-connection pools
- **Invalidation files**: PHP files with connection status, written to configured path

## Integration Notes

- **Cache backends**: Works with Memcache, Redis - configure cache to not use DB
- **Session storage**: Configure session to not use DB for better performance
- **Logging**: Use syslog instead of dblog if `'watchdog on shutdown' => FALSE`

## Drupal 10 Compatibility & Security Guidelines

### Critical Deprecated Code Patterns (Must Fix)

**1. Deprecated Functions & Constants:**

```php
// DEPRECATED - Remove or replace
drupal_is_cli()                    // Use PHP_SAPI === 'cli' or \Drupal::service('string_translation')->isCliMode()
$_SERVER['REQUEST_TIME']           // Use \Drupal::time()->getRequestTime()
$_GLOBALS['variable_name']         // Use \Drupal::state() or \Drupal::config()
DRUPAL_TEST_IN_CHILD_SITE         // Use environment variables or test detection service
FILE_EXISTS_REPLACE               // Use \Drupal\Core\File\FileSystemInterface::EXISTS_REPLACE

// CORRECT REPLACEMENTS
if (PHP_SAPI === 'cli' || basename($_SERVER['PHP_SELF']) == 'update.php')
$request_time = \Drupal::time()->getRequestTime()
```

**2. Session Access (Properly Wrapped):**

```php
// CORRECT - Already using proper checks
if (\Drupal::hasRequest() &&
    \Drupal::request()->hasSession() &&
    \Drupal::request()->getSession()->isStarted()) {
  $_SESSION['key'] = $value;
}
```

### Security Vulnerabilities & Fixes

**1. File Include Vulnerability (CRITICAL):**

```php
// VULNERABLE CODE in loadInvalidationFile() and updateInvalidationFile()
include $file;  // Direct include without validation!

// FIX: Validate file exists and is in expected location
if (isset($this->connectionOptions['invalidation path'])) {
  $file = $this->connectionOptions['invalidation path'] . "/autoslave-invalidation-$key.inc";
  // Validate file is within invalidation path (path traversal protection)
  $realpath = realpath(dirname($file));
  $basepath = realpath($this->connectionOptions['invalidation path']);
  if ($realpath !== FALSE && $basepath !== FALSE &&
      strpos($realpath, $basepath) === 0 && file_exists($file)) {
    include $file;
  }
}
```

**2. Variable Injection in Generated Files:**

```php
// CURRENT CODE - Potential risk
$output .= '$databases["' . $key . '"]["' . $target . '"][' . $idx . ']["status"] = FALSE;' . "\n";

// BETTER: Use var_export for safety
if (isset($databases[$key][$target][$idx]['status'])) {
  $output .= sprintf(
    '$databases[%s][%s][%d]["status"] = FALSE;' . "\n",
    var_export($key, TRUE),
    var_export($target, TRUE),
    (int) $idx
  );
}
```

**3. SQL Injection Protection:**

- All query routing is safe - uses Drupal's prepared statements
- Table names from `getTables()` are already prefixed/escaped by core
- User input never directly interpolated into queries

**4. Input Validation Required:**

```php
// ADD validation for connection pool configuration
// In setupConnections(), validate weight is numeric and positive
$conninfo['weight'] = isset($conninfo['weight']) ? max(1, (int) $conninfo['weight']) : 100;
```

### PHP 8.1+ Compatibility Issues

**1. Missing Return Type Declarations:**

```php
// ADD return types to all public methods (Drupal 10.2+ requirement)
public function driver(): string { return 'autoslave'; }
public function getPool(): array { return $this->__pool; }
public function forceMaster(?int $force_master = NULL): int { ... }
```

**2. Deprecated Dynamic Properties:**

```php
// All class properties should be declared
// AutoslaveAffectedTables class needs property declaration
class AutoslaveAffectedTables {
  protected ?Connection $connection = NULL;  // Add type hint
}
```

**3. Null Coalescing Operator Usage:**

```php
// Already correctly using ?? operator throughout
$version = $_SESSION['autoslave_affected_tables_version'] ?? '1.3';
```

### Drupal Coding Standards Compliance

**1. Use Dependency Injection (Not Static Calls):**

```php
// AVOID in controllers/services
\Drupal::service('file_system')
\Drupal::logger('autoslave')
\Drupal::messenger()

// CORRECT: Inject in __construct() and use $this->fileSystem, $this->logger
public function __construct(
  FileSystemInterface $file_system,
  LoggerChannelInterface $logger,
  MessengerInterface $messenger
) { ... }
```

**2. Type Hinting Requirements:**

```php
// ADD to all method signatures
public function updateInvalidationFile(string $key, string $target, int $idx, bool $status): void
protected function setupConnections(): void
public function getAffectedTables(int $expires = 0): array
```

**3. Proper Exception Handling:**

```php
// Use typed catch blocks
catch (\Exception $e) {  // Too broad
// BETTER:
catch (DatabaseException $e) {
catch (FileException $e) {
```

### Testing Requirements for Drupal 10

**1. PHPUnit 9.x/10.x Compatibility:**

- Update test base classes from `DrupalTestCase` to `KernelTestBase` or `BrowserTestBase`
- Use proper namespaces: `Drupal\Tests\autoslave\Kernel\`
- Deprecation testing enabled in CI (see `.gitlab-ci.yml`)

**2. SimpleTest Removal:**

- SimpleTest fully removed in Drupal 10
- Batch alter hook references SimpleTest - consider updating or removing
- Use PHPUnit's data providers instead

**3. Required Test Coverage:**

- Connection pool selection and failover
- Master/slave routing logic
- Affected tables tracking (session and database modes)
- Lock backend integration
- Invalidation file handling (with security fixes)

### Performance & Best Practices

**1. Reduce Static Service Calls:**
Replace `\Drupal::service()` calls in loops with class properties

**2. Lazy Loading:**
Already correctly implemented for affected tables backend

**3. Cache Affected Tables Lookups:**
Consider adding runtime cache for `getAffectedTables()` results within same request

**4. Deprecation Handling:**

```php
// Use @trigger_error for internal deprecations
if (method_exists($this, 'oldMethod')) {
  @trigger_error('oldMethod() is deprecated in autoslave:8.x-1.0. Use newMethod() instead.', E_USER_DEPRECATED);
}
```

### Migration Checklist for Drupal 10

- [ ] Replace all `drupal_is_cli()` with `PHP_SAPI === 'cli'`
- [ ] Replace `$_SERVER['REQUEST_TIME']` with `\Drupal::time()->getRequestTime()`
- [ ] Fix file include vulnerability with path validation
- [ ] Add variable sanitization to generated invalidation files
- [ ] Add return type declarations to all public methods
- [ ] Convert static `\Drupal::` calls to dependency injection in services
- [ ] Add strict type declarations: `declare(strict_types=1);` at top of files
- [ ] Update all PHPUnit tests to use proper base classes
- [ ] Remove or update SimpleTest references
- [ ] Add comprehensive deprecation tests
- [ ] Validate against PHP 8.1+ and 8.2+ in CI
- [ ] Test with Symfony 6.x components
