Skip to content

Adminer Cheatsheet

Adminer - Database Management Tool

Adminer (formerly phpMinAdmin) is a full-featured database management tool written in PHP. It consists of a single PHP file ready to deploy to the target server. Adminer is available for MySQL, MariaDB, PostgreSQL, SQLite, MS SQL, Oracle, Elasticsearch, MongoDB, and more.

Table of Contents

Installation

Basic Installation

# Download latest version
wget https://www.adminer.org/latest.php -O adminer.php

# Or download specific version
wget https://github.com/vrana/adminer/releases/download/v4.8.1/adminer-4.8.1.php -O adminer.php

# Make it accessible via web server
sudo cp adminer.php /var/www/html/
sudo chown www-data:www-data /var/www/html/adminer.php
sudo chmod 644 /var/www/html/adminer.php

# Access via browser
# http://your-server.com/adminer.php

Docker Installation

# Run Adminer in Docker
docker run --name adminer -p 8080:8080 adminer

# With specific tag
docker run --name adminer -p 8080:8080 adminer:4.8.1

# With custom configuration
docker run --name adminer \
  -p 8080:8080 \
  -e ADMINER_DEFAULT_SERVER=mysql \
  -e ADMINER_DESIGN=pepa-linha \
  adminer

# Access at http://localhost:8080

# Docker Compose setup
version: '3.8'
services:
  adminer:
    image: adminer:4.8.1
    restart: always
    ports:
      - 8080:8080
    environment:
      ADMINER_DEFAULT_SERVER: mysql
      ADMINER_DESIGN: pepa-linha
    volumes:
      - ./adminer-plugins:/var/www/html/plugins-enabled

Web Server Configuration

# Apache configuration
<VirtualHost *:80>
    ServerName adminer.example.com
    DocumentRoot /var/www/adminer

    <Directory /var/www/adminer>
        Options -Indexes
        AllowOverride All
        Require all granted

        # Security headers
        Header always set X-Content-Type-Options nosniff
        Header always set X-Frame-Options DENY
        Header always set X-XSS-Protection "1; mode=block"
    </Directory>

    # Restrict access to specific IPs
    <Location />
        Require ip 192.168.1.0/24
        Require ip 10.0.0.0/8
    </Location>
</VirtualHost>
# Nginx configuration
server {
    listen 80;
    server_name adminer.example.com;
    root /var/www/adminer;
    index adminer.php;

    # Security headers
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options DENY;
    add_header X-XSS-Protection "1; mode=block";

    # Restrict access
    allow 192.168.1.0/24;
    allow 10.0.0.0/8;
    deny all;

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny access to sensitive files
    location ~ /\. {
        deny all;
    }
}

PHP Requirements

<?php
// Check PHP requirements
echo "PHP Version: " . PHP_VERSION . "\n";

// Required extensions
$required_extensions = [
    'pdo',
    'pdo_mysql',    // For MySQL/MariaDB
    'pdo_pgsql',    // For PostgreSQL
    'pdo_sqlite',   // For SQLite
    'pdo_sqlsrv',   // For SQL Server
    'pdo_oci',      // For Oracle
    'mongodb',      // For MongoDB
    'json',
    'session',
    'pcre'
];

foreach ($required_extensions as $ext) {
    if (extension_loaded($ext)) {
        echo "✓ $ext extension loaded\n";
    } else {
        echo "✗ $ext extension NOT loaded\n";
    }
}

// Memory and time limits
echo "Memory Limit: " . ini_get('memory_limit') . "\n";
echo "Max Execution Time: " . ini_get('max_execution_time') . "\n";
echo "Upload Max Filesize: " . ini_get('upload_max_filesize') . "\n";
echo "Post Max Size: " . ini_get('post_max_size') . "\n";
?>

Configuration

Basic Configuration

<?php
// adminer-config.php - Custom configuration file

// Include original Adminer
include './adminer.php';

// Custom configuration class
class AdminerConfig extends Adminer {

    // Default database server
    function database() {
        return 'myapp_db';
    }

    // Default username
    function username() {
        return 'admin';
    }

    // Hide certain databases
    function databases($flush = true) {
        return array('myapp_db', 'analytics_db');
    }

    // Custom login credentials
    function login($login, $password) {
        // Allow specific users
        $users = array(
            'admin' => 'secure_password_hash',
            'readonly' => 'readonly_password_hash'
        );

        return isset($users[$login]) && 
               password_verify($password, $users[$login]);
    }

    // Customize interface
    function name() {
        return 'MyApp Database Admin';
    }

    // Custom CSS
    function head() {
        echo '<style>
            body { font-family: Arial, sans-serif; }
            #menu { background: #2c3e50; }
            #menu a { color: white; }
        </style>';
    }
}

// Use custom configuration
new AdminerConfig;
?>

Environment-Specific Configuration

<?php
// config/development.php
class AdminerDevelopment extends Adminer {
    function login($login, $password) {
        // Allow any login in development
        return true;
    }

    function database() {
        return 'myapp_dev';
    }

    function name() {
        return 'Development Database';
    }
}

// config/production.php
class AdminerProduction extends Adminer {
    function login($login, $password) {
        // Strict authentication in production
        $allowed_users = [
            'dba' => '$2y$10$hash_for_dba_password',
            'readonly' => '$2y$10$hash_for_readonly_password'
        ];

        return isset($allowed_users[$login]) && 
               password_verify($password, $allowed_users[$login]);
    }

    function databases() {
        // Only show production databases
        return ['myapp_prod', 'analytics_prod'];
    }

    function name() {
        return 'Production Database - CAUTION';
    }

    // Disable dangerous operations
    function drop() {
        return false; // Disable DROP operations
    }

    function alter() {
        return false; // Disable ALTER operations
    }
}

// Load environment-specific config
$environment = $_ENV['APP_ENV'] ?? 'development';
include "config/{$environment}.php";
?>

Plugin System

<?php
// plugins-enabled/plugin.php
// Enable specific plugins

// Available plugins:
// - designs (themes)
// - dump-date (add date to exports)
// - dump-zip (compress exports)
// - edit-calendar (date picker)
// - edit-foreign (foreign key editor)
// - edit-textarea (larger text areas)
// - email-table (email table contents)
// - enum-option (enum field editor)
// - file-upload (file upload interface)
// - foreign-system (cross-database foreign keys)
// - frames (frame interface)
// - json-column (JSON column editor)
// - login-password-less (passwordless login)
// - login-servers (predefined servers)
// - login-ssl (SSL connection options)
// - master-password (master password protection)
// - readonly (read-only mode)
// - slug (SEO-friendly URLs)
// - struct-comments (structure comments)
// - table-structure (enhanced table structure)
// - tinymce (rich text editor)
// - translation (custom translations)
// - version-noverify (skip version verification)

// Enable plugins
$plugins = [
    'designs',
    'edit-calendar',
    'edit-textarea',
    'dump-zip',
    'json-column'
];

foreach ($plugins as $plugin) {
    if (file_exists("plugins/$plugin.php")) {
        include "plugins/$plugin.php";
    }
}
?>

Database Connections

Connection Parameters

<?php
// Connection configuration for different databases

// MySQL/MariaDB connection
$mysql_config = [
    'server' => 'localhost:3306',
    'username' => 'dbuser',
    'password' => 'password',
    'database' => 'myapp',
    'driver' => 'mysql'
];

// PostgreSQL connection
$pgsql_config = [
    'server' => 'localhost:5432',
    'username' => 'postgres',
    'password' => 'password',
    'database' => 'myapp',
    'driver' => 'pgsql'
];

// SQLite connection
$sqlite_config = [
    'server' => '',
    'username' => '',
    'password' => '',
    'database' => '/path/to/database.sqlite',
    'driver' => 'sqlite'
];

// SQL Server connection
$sqlsrv_config = [
    'server' => 'localhost:1433',
    'username' => 'sa',
    'password' => 'password',
    'database' => 'myapp',
    'driver' => 'mssql'
];

// Oracle connection
$oracle_config = [
    'server' => 'localhost:1521/XE',
    'username' => 'hr',
    'password' => 'password',
    'database' => '',
    'driver' => 'oracle'
];
?>

SSL/TLS Configuration

<?php
// SSL connection configuration
class AdminerSSL extends Adminer {
    function connectSsl() {
        return [
            MYSQLI_CLIENT_SSL => true,
            MYSQLI_CLIENT_SSL_VERIFY_SERVER_CERT => true
        ];
    }

    function credentials() {
        return [
            'server' => 'secure-db.example.com:3306',
            'username' => 'ssl_user',
            'password' => 'ssl_password',
            'ssl' => [
                'key' => '/path/to/client-key.pem',
                'cert' => '/path/to/client-cert.pem',
                'ca' => '/path/to/ca-cert.pem',
                'capath' => '/path/to/ca-certs/',
                'cipher' => 'DHE-RSA-AES256-SHA'
            ]
        ];
    }
}

// PostgreSQL SSL configuration
$pgsql_ssl_dsn = "pgsql:host=secure-db.example.com;port=5432;dbname=myapp;sslmode=require;sslcert=/path/to/client.crt;sslkey=/path/to/client.key;sslrootcert=/path/to/ca.crt";
?>

Connection Pooling

<?php
// Connection management
class AdminerConnectionPool extends Adminer {
    private static $connections = [];

    function connect() {
        $server = $this->server();
        $username = $this->username();
        $password = $this->password();

        $key = md5($server . $username . $password);

        if (!isset(self::$connections[$key])) {
            self::$connections[$key] = new PDO(
                $this->dsn(),
                $username,
                $password,
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_PERSISTENT => true,
                    PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4"
                ]
            );
        }

        return self::$connections[$key];
    }

    function dsn() {
        $server = $this->server();
        $database = $this->database();

        switch ($this->driver()) {
            case 'mysql':
                return "mysql:host=$server;dbname=$database;charset=utf8mb4";
            case 'pgsql':
                return "pgsql:host=$server;dbname=$database";
            case 'sqlite':
                return "sqlite:$database";
            default:
                return parent::dsn();
        }
    }
}
?>

Database Operations

Database Management

-- Create database
CREATE DATABASE myapp_new 
CHARACTER SET utf8mb4 
COLLATE utf8mb4_unicode_ci;

-- PostgreSQL
CREATE DATABASE myapp_new 
WITH ENCODING 'UTF8' 
LC_COLLATE='en_US.UTF-8' 
LC_CTYPE='en_US.UTF-8';

-- Drop database
DROP DATABASE myapp_old;

-- Show databases
SHOW DATABASES;

-- PostgreSQL
SELECT datname FROM pg_database;

-- Database size information
SELECT 
    table_schema as database_name,
    ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) as size_mb
FROM information_schema.tables
GROUP BY table_schema;

-- PostgreSQL database size
SELECT 
    datname as database_name,
    pg_size_pretty(pg_database_size(datname)) as size
FROM pg_database
WHERE datistemplate = false;

Schema Operations

-- Create schema (PostgreSQL)
CREATE SCHEMA analytics;
CREATE SCHEMA reporting;

-- Set search path
SET search_path TO analytics, public;

-- Grant schema permissions
GRANT USAGE ON SCHEMA analytics TO app_user;
GRANT CREATE ON SCHEMA analytics TO app_user;

-- List schemas
SELECT schema_name 
FROM information_schema.schemata 
WHERE schema_name NOT IN ('information_schema', 'pg_catalog');

-- Schema size information
SELECT 
    schemaname,
    COUNT(*) as table_count,
    SUM(pg_total_relation_size(schemaname||'.'||tablename)) as total_size
FROM pg_tables 
WHERE schemaname NOT IN ('information_schema', 'pg_catalog')
GROUP BY schemaname;

Backup and Restore

# MySQL backup via Adminer
# Use Export functionality in web interface
# Or command line:
mysqldump -u username -p --single-transaction --routines --triggers myapp > backup.sql

# PostgreSQL backup
pg_dump -U username -h localhost -d myapp -f backup.sql

# SQLite backup
sqlite3 myapp.db ".backup backup.db"

# Restore MySQL
mysql -u username -p myapp < backup.sql

# Restore PostgreSQL
psql -U username -h localhost -d myapp -f backup.sql

# Restore SQLite
sqlite3 myapp.db ".restore backup.db"

Table Management

Table Creation

-- Create table with various data types
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    date_of_birth DATE,
    is_active BOOLEAN DEFAULT TRUE,
    profile_data JSON,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_email (email),
    INDEX idx_name (first_name, last_name),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- PostgreSQL table with advanced features
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id),
    order_number VARCHAR(20) UNIQUE NOT NULL,
    status order_status_enum DEFAULT 'pending',
    total_amount DECIMAL(10,2) NOT NULL CHECK (total_amount >= 0),
    order_data JSONB,
    shipping_address TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),

    CONSTRAINT chk_order_number CHECK (order_number ~ '^ORD-[0-9]{6}$')
);

-- Create indexes
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
CREATE INDEX idx_orders_data_gin ON orders USING GIN (order_data);

-- Create enum type (PostgreSQL)
CREATE TYPE order_status_enum AS ENUM ('pending', 'processing', 'shipped', 'delivered', 'cancelled');

Table Modification

-- Add column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users ADD COLUMN last_login TIMESTAMP NULL;

-- Modify column
ALTER TABLE users MODIFY COLUMN phone VARCHAR(25);
-- PostgreSQL
ALTER TABLE users ALTER COLUMN phone TYPE VARCHAR(25);

-- Drop column
ALTER TABLE users DROP COLUMN phone;

-- Add constraint
ALTER TABLE users ADD CONSTRAINT uk_email UNIQUE (email);
ALTER TABLE orders ADD CONSTRAINT fk_orders_user_id 
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;

-- Drop constraint
ALTER TABLE users DROP CONSTRAINT uk_email;
ALTER TABLE orders DROP CONSTRAINT fk_orders_user_id;

-- Rename table
RENAME TABLE old_table TO new_table;
-- PostgreSQL
ALTER TABLE old_table RENAME TO new_table;

-- Rename column
ALTER TABLE users CHANGE old_column new_column VARCHAR(50);
-- PostgreSQL
ALTER TABLE users RENAME COLUMN old_column TO new_column;

Index Management

-- Create indexes
CREATE INDEX idx_users_email ON users(email);
CREATE UNIQUE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_name ON users(first_name, last_name);

-- Composite indexes
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
CREATE INDEX idx_orders_date_status ON orders(created_at, status);

-- Partial indexes (PostgreSQL)
CREATE INDEX idx_active_users ON users(email) WHERE is_active = true;
CREATE INDEX idx_recent_orders ON orders(created_at) 
    WHERE created_at >= NOW() - INTERVAL '30 days';

-- Functional indexes
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
CREATE INDEX idx_orders_year ON orders(EXTRACT(YEAR FROM created_at));

-- Full-text indexes (MySQL)
CREATE FULLTEXT INDEX idx_products_search ON products(name, description);

-- GIN indexes for JSON (PostgreSQL)
CREATE INDEX idx_users_profile_gin ON users USING GIN (profile_data);

-- Show indexes
SHOW INDEXES FROM users;
-- PostgreSQL
SELECT * FROM pg_indexes WHERE tablename = 'users';

-- Drop index
DROP INDEX idx_users_email ON users;
-- PostgreSQL
DROP INDEX idx_users_email;

-- Index usage statistics (PostgreSQL)
SELECT 
    schemaname,
    tablename,
    indexname,
    idx_scan,
    idx_tup_read,
    idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;

Data Operations

Data Insertion

-- Single row insert
INSERT INTO users (username, email, password_hash, first_name, last_name) 
VALUES ('john_doe', 'john@example.com', 'hashed_password', 'John', 'Doe');

-- Multiple row insert
INSERT INTO users (username, email, password_hash, first_name, last_name) VALUES
('jane_smith', 'jane@example.com', 'hashed_password', 'Jane', 'Smith'),
('bob_wilson', 'bob@example.com', 'hashed_password', 'Bob', 'Wilson'),
('alice_brown', 'alice@example.com', 'hashed_password', 'Alice', 'Brown');

-- Insert with JSON data (MySQL 5.7+)
INSERT INTO users (username, email, profile_data) VALUES
('user1', 'user1@example.com', '{"preferences": {"theme": "dark", "language": "en"}, "settings": {"notifications": true}}');

-- Insert with JSONB (PostgreSQL)
INSERT INTO orders (user_id, order_number, order_data) VALUES
(1, 'ORD-000001', '{"items": [{"product_id": 1, "quantity": 2, "price": 29.99}], "shipping": {"method": "standard", "cost": 5.99}}');

-- Insert from SELECT
INSERT INTO archived_orders (user_id, order_number, total_amount, created_at)
SELECT user_id, order_number, total_amount, created_at
FROM orders
WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);

-- Insert with ON DUPLICATE KEY UPDATE (MySQL)
INSERT INTO user_stats (user_id, login_count, last_login)
VALUES (1, 1, NOW())
ON DUPLICATE KEY UPDATE 
    login_count = login_count + 1,
    last_login = NOW();

-- Insert with UPSERT (PostgreSQL)
INSERT INTO user_stats (user_id, login_count, last_login)
VALUES (1, 1, NOW())
ON CONFLICT (user_id) DO UPDATE SET
    login_count = user_stats.login_count + 1,
    last_login = NOW();

Data Updates

-- Simple update
UPDATE users 
SET email = 'newemail@example.com' 
WHERE id = 1;

-- Update with conditions
UPDATE users 
SET is_active = FALSE, updated_at = NOW()
WHERE last_login < DATE_SUB(NOW(), INTERVAL 6 MONTH);

-- Update with JOIN
UPDATE users u
JOIN user_profiles p ON u.id = p.user_id
SET u.first_name = p.display_name
WHERE p.display_name IS NOT NULL;

-- Update JSON data (MySQL)
UPDATE users 
SET profile_data = JSON_SET(profile_data, '$.preferences.theme', 'light')
WHERE id = 1;

-- Update JSONB data (PostgreSQL)
UPDATE orders 
SET order_data = order_data || '{"status": "updated"}'::jsonb
WHERE id = 1;

-- Conditional update
UPDATE products 
SET price = CASE 
    WHEN category = 'electronics' THEN price * 1.1
    WHEN category = 'books' THEN price * 1.05
    ELSE price
END
WHERE updated_at < DATE_SUB(NOW(), INTERVAL 1 MONTH);

-- Update with subquery
UPDATE users 
SET total_orders = (
    SELECT COUNT(*) 
    FROM orders 
    WHERE orders.user_id = users.id
);

-- Bulk update with temporary table
CREATE TEMPORARY TABLE temp_updates (
    user_id INT,
    new_email VARCHAR(100)
);

INSERT INTO temp_updates VALUES
(1, 'user1_new@example.com'),
(2, 'user2_new@example.com');

UPDATE users u
JOIN temp_updates t ON u.id = t.user_id
SET u.email = t.new_email;

Data Deletion

-- Simple delete
DELETE FROM users WHERE id = 1;

-- Delete with conditions
DELETE FROM logs 
WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);

-- Delete with JOIN
DELETE u FROM users u
JOIN user_profiles p ON u.id = p.user_id
WHERE p.status = 'deleted';

-- Delete with subquery
DELETE FROM orders 
WHERE user_id IN (
    SELECT id FROM users WHERE is_active = FALSE
);

-- Safe delete with LIMIT
DELETE FROM temp_data 
WHERE processed = TRUE 
ORDER BY created_at 
LIMIT 1000;

-- Soft delete (mark as deleted)
UPDATE users 
SET is_deleted = TRUE, deleted_at = NOW()
WHERE id = 1;

-- Cascade delete (with foreign key constraints)
DELETE FROM users WHERE id = 1;
-- This will also delete related orders if FK has ON DELETE CASCADE

-- Truncate table (fast delete all)
TRUNCATE TABLE temp_table;

-- Delete duplicates
DELETE u1 FROM users u1
INNER JOIN users u2 
WHERE u1.id > u2.id 
  AND u1.email = u2.email;

-- PostgreSQL delete duplicates
DELETE FROM users 
WHERE id NOT IN (
    SELECT MIN(id) 
    FROM users 
    GROUP BY email
);

Data Querying

-- Basic SELECT
SELECT id, username, email, created_at 
FROM users 
WHERE is_active = TRUE
ORDER BY created_at DESC
LIMIT 10;

-- Complex WHERE conditions
SELECT * FROM orders 
WHERE (status = 'pending' OR status = 'processing')
  AND total_amount > 100
  AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY);

-- JOIN operations
SELECT 
    u.username,
    u.email,
    COUNT(o.id) as order_count,
    SUM(o.total_amount) as total_spent,
    MAX(o.created_at) as last_order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.is_active = TRUE
GROUP BY u.id, u.username, u.email
HAVING COUNT(o.id) > 0
ORDER BY total_spent DESC;

-- Subqueries
SELECT * FROM users 
WHERE id IN (
    SELECT DISTINCT user_id 
    FROM orders 
    WHERE total_amount > 500
);

-- EXISTS subquery
SELECT * FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o 
    WHERE o.user_id = u.id 
      AND o.status = 'completed'
);

-- Window functions (PostgreSQL/MySQL 8.0+)
SELECT 
    username,
    email,
    created_at,
    ROW_NUMBER() OVER (ORDER BY created_at) as user_number,
    RANK() OVER (ORDER BY created_at) as user_rank,
    LAG(created_at) OVER (ORDER BY created_at) as previous_user_date
FROM users;

-- JSON queries (MySQL)
SELECT 
    id,
    username,
    JSON_EXTRACT(profile_data, '$.preferences.theme') as theme,
    JSON_EXTRACT(profile_data, '$.settings.notifications') as notifications
FROM users
WHERE JSON_EXTRACT(profile_data, '$.preferences.theme') = 'dark';

-- JSONB queries (PostgreSQL)
SELECT 
    id,
    order_number,
    order_data->>'status' as status,
    order_data->'items'->0->>'product_id' as first_product_id
FROM orders
WHERE order_data @> '{"status": "completed"}';

-- Full-text search (MySQL)
SELECT * FROM products 
WHERE MATCH(name, description) AGAINST('laptop gaming' IN NATURAL LANGUAGE MODE);

-- Full-text search (PostgreSQL)
SELECT * FROM products 
WHERE to_tsvector('english', name || ' ' || description) @@ to_tsquery('english', 'laptop & gaming');

SQL Editor

Query Execution

-- Adminer SQL editor features:
-- 1. Syntax highlighting
-- 2. Auto-completion
-- 3. Query history
-- 4. Multiple query execution
-- 5. Export results
-- 6. Query profiling

-- Execute multiple queries (separate with semicolons)
SELECT COUNT(*) as total_users FROM users;
SELECT COUNT(*) as total_orders FROM orders;
SELECT COUNT(*) as total_products FROM products;

-- Query with parameters (use Adminer's parameter feature)
SELECT * FROM users 
WHERE created_at >= ? 
  AND status = ?;
-- Parameters: ['2023-01-01', 'active']

-- Complex analytical query
WITH monthly_stats AS (
    SELECT 
        DATE_FORMAT(created_at, '%Y-%m') as month,
        COUNT(*) as order_count,
        SUM(total_amount) as revenue,
        AVG(total_amount) as avg_order_value
    FROM orders
    WHERE created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
    GROUP BY DATE_FORMAT(created_at, '%Y-%m')
),
growth_stats AS (
    SELECT 
        month,
        order_count,
        revenue,
        avg_order_value,
        LAG(revenue) OVER (ORDER BY month) as prev_month_revenue,
        (revenue - LAG(revenue) OVER (ORDER BY month)) / LAG(revenue) OVER (ORDER BY month) * 100 as growth_rate
    FROM monthly_stats
)
SELECT 
    month,
    order_count,
    ROUND(revenue, 2) as revenue,
    ROUND(avg_order_value, 2) as avg_order_value,
    ROUND(growth_rate, 2) as growth_rate_percent
FROM growth_stats
ORDER BY month;

Query Optimization

-- Use EXPLAIN to analyze query performance
EXPLAIN SELECT 
    u.username,
    COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2023-01-01'
GROUP BY u.id, u.username;

-- PostgreSQL EXPLAIN with detailed analysis
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) 
SELECT 
    u.username,
    COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2023-01-01'
GROUP BY u.id, u.username;

-- Optimize with proper indexes
CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- Use LIMIT for large result sets
SELECT * FROM large_table 
ORDER BY created_at DESC 
LIMIT 100;

-- Use EXISTS instead of IN for better performance
SELECT * FROM users u
WHERE EXISTS (
    SELECT 1 FROM orders o 
    WHERE o.user_id = u.id
) 
LIMIT 1000;

-- Optimize GROUP BY queries
SELECT 
    status,
    COUNT(*) as count
FROM orders
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY status
ORDER BY count DESC;

-- Use covering indexes
CREATE INDEX idx_orders_covering ON orders(status, created_at, total_amount);

-- Query with index hints (MySQL)
SELECT * FROM users 
USE INDEX (idx_users_email)
WHERE email = 'user@example.com';

-- Force index usage
SELECT * FROM users 
FORCE INDEX (idx_users_created_at)
WHERE created_at >= '2023-01-01'
ORDER BY created_at;

Stored Procedures and Functions

-- MySQL stored procedure
DELIMITER //
CREATE PROCEDURE GetUserStats(IN user_id INT)
BEGIN
    SELECT 
        u.username,
        u.email,
        COUNT(o.id) as total_orders,
        SUM(o.total_amount) as total_spent,
        AVG(o.total_amount) as avg_order_value,
        MAX(o.created_at) as last_order_date
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.id = user_id
    GROUP BY u.id, u.username, u.email;
END //
DELIMITER ;

-- Call stored procedure
CALL GetUserStats(1);

-- MySQL function
DELIMITER //
CREATE FUNCTION CalculateDiscount(order_total DECIMAL(10,2)) 
RETURNS DECIMAL(10,2)
READS SQL DATA
DETERMINISTIC
BEGIN
    DECLARE discount DECIMAL(10,2) DEFAULT 0;

    IF order_total >= 1000 THEN
        SET discount = order_total * 0.10;
    ELSEIF order_total >= 500 THEN
        SET discount = order_total * 0.05;
    ELSEIF order_total >= 100 THEN
        SET discount = order_total * 0.02;
    END IF;

    RETURN discount;
END //
DELIMITER ;

-- Use function
SELECT 
    id,
    total_amount,
    CalculateDiscount(total_amount) as discount,
    total_amount - CalculateDiscount(total_amount) as final_amount
FROM orders;

-- PostgreSQL function
CREATE OR REPLACE FUNCTION get_user_order_summary(p_user_id INTEGER)
RETURNS TABLE(
    username VARCHAR,
    email VARCHAR,
    total_orders BIGINT,
    total_spent NUMERIC,
    avg_order_value NUMERIC
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        u.username,
        u.email,
        COUNT(o.id)::BIGINT,
        COALESCE(SUM(o.total_amount), 0),
        COALESCE(AVG(o.total_amount), 0)
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.id = p_user_id
    GROUP BY u.username, u.email;
END;
$$ LANGUAGE plpgsql;

-- Call PostgreSQL function
SELECT * FROM get_user_order_summary(1);

Import/Export

Data Import

<?php
// CSV import configuration in Adminer
$import_config = [
    'format' => 'csv',
    'delimiter' => ',',
    'enclosure' => '"',
    'escape' => '\\',
    'charset' => 'utf-8',
    'skip_first_line' => true,
    'null_value' => '',
    'date_format' => 'Y-m-d',
    'datetime_format' => 'Y-m-d H:i:s'
];

// SQL import example
$sql_import = "
INSERT INTO users (username, email, first_name, last_name) VALUES
('user1', 'user1@example.com', 'John', 'Doe'),
('user2', 'user2@example.com', 'Jane', 'Smith'),
('user3', 'user3@example.com', 'Bob', 'Wilson');

INSERT INTO orders (user_id, order_number, total_amount) VALUES
(1, 'ORD-001', 99.99),
(2, 'ORD-002', 149.99),
(3, 'ORD-003', 79.99);
";
?>
-- Bulk import strategies

-- MySQL LOAD DATA INFILE
LOAD DATA INFILE '/path/to/users.csv'
INTO TABLE users
FIELDS TERMINATED BY ','
ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS
(username, email, first_name, last_name, @created_at)
SET created_at = STR_TO_DATE(@created_at, '%m/%d/%Y %H:%i:%s');

-- PostgreSQL COPY
COPY users(username, email, first_name, last_name, created_at)
FROM '/path/to/users.csv'
DELIMITER ','
CSV HEADER;

-- Import with data transformation
LOAD DATA INFILE '/path/to/orders.csv'
INTO TABLE orders
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
IGNORE 1 ROWS
(user_id, @order_date, @total_amount, status)
SET 
    order_date = STR_TO_DATE(@order_date, '%m/%d/%Y'),
    total_amount = CAST(@total_amount AS DECIMAL(10,2)),
    created_at = NOW();

-- Handle import errors
LOAD DATA INFILE '/path/to/data.csv'
INTO TABLE temp_import
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
IGNORE 1 ROWS;

-- Validate imported data
SELECT COUNT(*) as total_imported FROM temp_import;
SELECT COUNT(*) as invalid_emails 
FROM temp_import 
WHERE email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';

-- Transfer valid data
INSERT INTO users (username, email, first_name, last_name)
SELECT username, email, first_name, last_name
FROM temp_import
WHERE email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
  AND username IS NOT NULL
  AND username != '';

Data Export

-- Export formats available in Adminer:
-- 1. SQL - INSERT statements
-- 2. CSV - Comma-separated values
-- 3. TSV - Tab-separated values
-- 4. XML - XML format
-- 5. JSON - JSON format

-- Export with custom query
SELECT 
    u.id,
    u.username,
    u.email,
    u.first_name,
    u.last_name,
    COUNT(o.id) as total_orders,
    SUM(o.total_amount) as total_spent,
    DATE_FORMAT(u.created_at, '%Y-%m-%d') as registration_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.is_active = TRUE
GROUP BY u.id
ORDER BY total_spent DESC;

-- Export specific date range
SELECT * FROM orders 
WHERE created_at >= '2023-01-01' 
  AND created_at < '2024-01-01'
ORDER BY created_at;

-- Export with data transformation
SELECT 
    id,
    username,
    CONCAT(first_name, ' ', last_name) as full_name,
    email,
    CASE 
        WHEN is_active = 1 THEN 'Active'
        ELSE 'Inactive'
    END as status,
    DATE_FORMAT(created_at, '%m/%d/%Y') as registration_date
FROM users;

-- Export JSON data (MySQL)
SELECT 
    id,
    username,
    email,
    JSON_PRETTY(profile_data) as profile_json
FROM users
WHERE profile_data IS NOT NULL;

-- Export aggregated data
SELECT 
    DATE_FORMAT(created_at, '%Y-%m') as month,
    COUNT(*) as order_count,
    SUM(total_amount) as total_revenue,
    AVG(total_amount) as avg_order_value,
    MIN(total_amount) as min_order,
    MAX(total_amount) as max_order
FROM orders
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
ORDER BY month;

Backup and Restore

<?php
// Automated backup script using Adminer
class AdminerBackup extends Adminer {

    function dumpTable($table, $style, $is_view = 0) {
        if ($style == 'CREATE') {
            // Add backup timestamp
            echo "-- Backup created: " . date('Y-m-d H:i:s') . "\n";
            echo "-- Table: $table\n\n";
        }
        return parent::dumpTable($table, $style, $is_view);
    }

    function dumpData($table, $style, $query) {
        if ($style == 'INSERT') {
            echo "-- Data for table: $table\n";
        }
        return parent::dumpData($table, $style, $query);
    }
}

// Backup configuration
$backup_config = [
    'format' => 'sql',
    'include_structure' => true,
    'include_data' => true,
    'include_drop' => true,
    'include_create' => true,
    'add_locks' => true,
    'hex_blob' => true,
    'routines' => true,
    'events' => true,
    'triggers' => true
];
?>
# Command-line backup scripts

# MySQL backup
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups"
DB_NAME="myapp"

mysqldump -u backup_user -p$BACKUP_PASSWORD \
  --single-transaction \
  --routines \
  --triggers \
  --events \
  --hex-blob \
  --add-drop-table \
  --add-locks \
  $DB_NAME > $BACKUP_DIR/${DB_NAME}_${DATE}.sql

# Compress backup
gzip $BACKUP_DIR/${DB_NAME}_${DATE}.sql

# Keep only last 7 days
find $BACKUP_DIR -name "${DB_NAME}_*.sql.gz" -mtime +7 -delete

# PostgreSQL backup
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups"
DB_NAME="myapp"

pg_dump -U backup_user -h localhost \
  --verbose \
  --clean \
  --create \
  --if-exists \
  --format=custom \
  --compress=9 \
  $DB_NAME > $BACKUP_DIR/${DB_NAME}_${DATE}.backup

# Keep only last 7 days
find $BACKUP_DIR -name "${DB_NAME}_*.backup" -mtime +7 -delete

# Restore MySQL
mysql -u username -p myapp < backup_file.sql

# Restore PostgreSQL
pg_restore -U username -h localhost -d myapp backup_file.backup

User Management

User Creation and Management

-- MySQL user management
-- Create user
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'secure_password';
CREATE USER 'app_user'@'%' IDENTIFIED BY 'secure_password';

-- Grant privileges
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'app_user'@'localhost';
GRANT ALL PRIVILEGES ON myapp.* TO 'admin_user'@'localhost';

-- Create role (MySQL 8.0+)
CREATE ROLE 'app_role';
GRANT SELECT, INSERT, UPDATE ON myapp.* TO 'app_role';
GRANT 'app_role' TO 'app_user'@'localhost';

-- Set default role
SET DEFAULT ROLE 'app_role' TO 'app_user'@'localhost';

-- Show user privileges
SHOW GRANTS FOR 'app_user'@'localhost';

-- Change password
ALTER USER 'app_user'@'localhost' IDENTIFIED BY 'new_password';

-- Account locking/unlocking
ALTER USER 'app_user'@'localhost' ACCOUNT LOCK;
ALTER USER 'app_user'@'localhost' ACCOUNT UNLOCK;

-- Password expiration
ALTER USER 'app_user'@'localhost' PASSWORD EXPIRE;
ALTER USER 'app_user'@'localhost' PASSWORD EXPIRE NEVER;

-- Drop user
DROP USER 'app_user'@'localhost';

-- PostgreSQL user management
-- Create user
CREATE USER app_user WITH PASSWORD 'secure_password';
CREATE USER readonly_user WITH PASSWORD 'readonly_password';

-- Create role
CREATE ROLE app_role;
CREATE ROLE readonly_role;

-- Grant privileges to role
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_role;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;

-- Grant role to user
GRANT app_role TO app_user;
GRANT readonly_role TO readonly_user;

-- Grant database privileges
GRANT CONNECT ON DATABASE myapp TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;

-- Change password
ALTER USER app_user WITH PASSWORD 'new_password';

-- Drop user
DROP USER app_user;

-- Show user information
SELECT 
    usename as username,
    usesuper as is_superuser,
    usecreatedb as can_create_db,
    userepl as can_replicate,
    valuntil as password_expiry
FROM pg_user;

Permission Management

-- Granular permission management

-- Table-level permissions
GRANT SELECT ON users TO 'readonly_user'@'localhost';
GRANT INSERT, UPDATE ON orders TO 'app_user'@'localhost';
GRANT DELETE ON logs TO 'admin_user'@'localhost';

-- Column-level permissions (MySQL 8.0+)
GRANT SELECT (id, username, email) ON users TO 'limited_user'@'localhost';
GRANT UPDATE (email, updated_at) ON users TO 'profile_editor'@'localhost';

-- PostgreSQL column-level permissions
GRANT SELECT (id, username, email) ON users TO limited_user;
GRANT UPDATE (email, updated_at) ON users TO profile_editor;

-- Stored procedure permissions
GRANT EXECUTE ON PROCEDURE GetUserStats TO 'app_user'@'localhost';
GRANT EXECUTE ON FUNCTION CalculateDiscount TO 'app_user'@'localhost';

-- View permissions
CREATE VIEW user_summary AS
SELECT id, username, email, created_at
FROM users
WHERE is_active = TRUE;

GRANT SELECT ON user_summary TO 'readonly_user'@'localhost';

-- Row-level security (PostgreSQL)
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY user_orders_policy ON orders
FOR ALL TO app_user
USING (user_id = current_setting('app.current_user_id')::INTEGER);

-- Set user context
SET app.current_user_id = '123';

-- Revoke permissions
REVOKE INSERT, UPDATE ON orders FROM 'app_user'@'localhost';
REVOKE ALL PRIVILEGES ON myapp.* FROM 'old_user'@'localhost';

-- Show effective permissions
SELECT 
    grantee,
    table_schema,
    table_name,
    privilege_type,
    is_grantable
FROM information_schema.table_privileges
WHERE grantee = 'app_user@localhost';

Security Policies

-- Password policies (MySQL 8.0+)
-- Install password validation plugin
INSTALL PLUGIN validate_password SONAME 'validate_password.so';

-- Configure password policy
SET GLOBAL validate_password.policy = STRONG;
SET GLOBAL validate_password.length = 12;
SET GLOBAL validate_password.mixed_case_count = 1;
SET GLOBAL validate_password.number_count = 1;
SET GLOBAL validate_password.special_char_count = 1;

-- Account management policies
CREATE USER 'temp_user'@'localhost' 
IDENTIFIED BY 'TempPassword123!' 
PASSWORD EXPIRE INTERVAL 30 DAY
FAILED_LOGIN_ATTEMPTS 3 
PASSWORD_LOCK_TIME 2;

-- Connection limits
ALTER USER 'app_user'@'localhost' 
WITH MAX_CONNECTIONS_PER_HOUR 1000
MAX_QUERIES_PER_HOUR 10000
MAX_UPDATES_PER_HOUR 5000;

-- PostgreSQL security policies
-- Create security-focused role
CREATE ROLE secure_app_role 
NOLOGIN 
NOCREATEDB 
NOCREATEROLE 
NOREPLICATION;

-- Grant minimal required privileges
GRANT CONNECT ON DATABASE myapp TO secure_app_role;
GRANT USAGE ON SCHEMA public TO secure_app_role;
GRANT SELECT, INSERT, UPDATE ON users TO secure_app_role;
GRANT SELECT ON products TO secure_app_role;

-- Create application user with secure role
CREATE USER app_user 
WITH PASSWORD 'SecurePassword123!' 
IN ROLE secure_app_role
VALID UNTIL '2024-12-31';

-- Audit user activities
CREATE TABLE user_audit_log (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50),
    action VARCHAR(50),
    table_name VARCHAR(50),
    record_id INTEGER,
    old_values JSONB,
    new_values JSONB,
    timestamp TIMESTAMP DEFAULT NOW(),
    ip_address INET
);

-- Audit trigger function
CREATE OR REPLACE FUNCTION audit_trigger_function()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO user_audit_log (
        username, action, table_name, record_id, 
        old_values, new_values, ip_address
    ) VALUES (
        current_user,
        TG_OP,
        TG_TABLE_NAME,
        COALESCE(NEW.id, OLD.id),
        CASE WHEN TG_OP = 'DELETE' THEN to_jsonb(OLD) ELSE NULL END,
        CASE WHEN TG_OP = 'INSERT' THEN to_jsonb(NEW) 
             WHEN TG_OP = 'UPDATE' THEN to_jsonb(NEW) 
             ELSE NULL END,
        inet_client_addr()
    );

    RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

-- Apply audit trigger to sensitive tables
CREATE TRIGGER users_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE FUNCTION audit_trigger_function();

Security

Access Control

<?php
// Secure Adminer configuration
class SecureAdminer extends Adminer {

    // IP whitelist
    private $allowed_ips = [
        '192.168.1.0/24',
        '10.0.0.0/8',
        '172.16.0.0/12'
    ];

    // Check IP access
    function __construct() {
        if (!$this->isAllowedIP()) {
            http_response_code(403);
            die('Access denied');
        }
    }

    private function isAllowedIP() {
        $client_ip = $_SERVER['REMOTE_ADDR'];

        foreach ($this->allowed_ips as $allowed_range) {
            if ($this->ipInRange($client_ip, $allowed_range)) {
                return true;
            }
        }

        return false;
    }

    private function ipInRange($ip, $range) {
        if (strpos($range, '/') === false) {
            return $ip === $range;
        }

        list($subnet, $mask) = explode('/', $range);
        return (ip2long($ip) & ~((1 << (32 - $mask)) - 1)) === ip2long($subnet);
    }

    // Secure login
    function login($login, $password) {
        // Rate limiting
        session_start();
        $attempts_key = 'login_attempts_' . $_SERVER['REMOTE_ADDR'];
        $attempts = $_SESSION[$attempts_key] ?? 0;

        if ($attempts >= 5) {
            $last_attempt = $_SESSION['last_attempt_' . $_SERVER['REMOTE_ADDR']] ?? 0;
            if (time() - $last_attempt < 300) { // 5 minutes lockout
                return false;
            } else {
                $_SESSION[$attempts_key] = 0; // Reset attempts
            }
        }

        // Validate credentials
        $valid_users = [
            'admin' => '$2y$10$hash_for_admin_password',
            'readonly' => '$2y$10$hash_for_readonly_password'
        ];

        if (isset($valid_users[$login]) && password_verify($password, $valid_users[$login])) {
            $_SESSION[$attempts_key] = 0; // Reset on successful login
            $_SESSION['user_role'] = $this->getUserRole($login);
            return true;
        } else {
            $_SESSION[$attempts_key] = $attempts + 1;
            $_SESSION['last_attempt_' . $_SERVER['REMOTE_ADDR']] = time();
            return false;
        }
    }

    private function getUserRole($login) {
        $roles = [
            'admin' => 'admin',
            'readonly' => 'readonly'
        ];
        return $roles[$login] ?? 'readonly';
    }

    // Restrict operations based on role
    function drop() {
        return $_SESSION['user_role'] === 'admin';
    }

    function alter() {
        return $_SESSION['user_role'] === 'admin';
    }

    function create() {
        return $_SESSION['user_role'] === 'admin';
    }

    // Hide sensitive databases
    function databases($flush = true) {
        $all_databases = parent::databases($flush);
        $allowed_databases = [
            'myapp_prod',
            'myapp_staging',
            'analytics'
        ];

        return array_intersect($all_databases, $allowed_databases);
    }

    // Log all activities
    function query($query, $start = null) {
        $result = parent::query($query, $start);

        // Log query
        $log_entry = [
            'timestamp' => date('Y-m-d H:i:s'),
            'user' => $this->username(),
            'ip' => $_SERVER['REMOTE_ADDR'],
            'query' => $query,
            'execution_time' => $start ? (microtime(true) - $start) : null
        ];

        file_put_contents(
            '/var/log/adminer/queries.log',
            json_encode($log_entry) . "\n",
            FILE_APPEND | LOCK_EX
        );

        return $result;
    }
}

// Use secure configuration
new SecureAdminer;
?>

Data Protection

<?php
// Data masking for sensitive information
class DataMaskingAdminer extends Adminer {

    private $sensitive_fields = [
        'password',
        'password_hash',
        'ssn',
        'credit_card',
        'phone',
        'email'
    ];

    // Mask sensitive data in select results
    function selectVal($val, $link, $field, $original) {
        if (in_array(strtolower($field['field']), $this->sensitive_fields)) {
            if ($field['field'] === 'email') {
                return $this->maskEmail($val);
            } elseif ($field['field'] === 'phone') {
                return $this->maskPhone($val);
            } else {
                return str_repeat('*', min(strlen($val), 8));
            }
        }

        return parent::selectVal($val, $link, $field, $original);
    }

    private function maskEmail($email) {
        if (strpos($email, '@') === false) {
            return $email;
        }

        list($local, $domain) = explode('@', $email);
        $masked_local = substr($local, 0, 2) . str_repeat('*', max(0, strlen($local) - 2));
        return $masked_local . '@' . $domain;
    }

    private function maskPhone($phone) {
        $cleaned = preg_replace('/[^0-9]/', '', $phone);
        if (strlen($cleaned) >= 4) {
            return str_repeat('*', strlen($cleaned) - 4) . substr($cleaned, -4);
        }
        return str_repeat('*', strlen($cleaned));
    }

    // Prevent export of sensitive data
    function dumpTable($table, $style, $is_view = 0) {
        if ($style === 'INSERT' && $this->hasSensitiveFields($table)) {
            echo "-- Export of sensitive table '$table' is restricted\n";
            return false;
        }

        return parent::dumpTable($table, $style, $is_view);
    }

    private function hasSensitiveFields($table) {
        $fields = fields($table);
        foreach ($fields as $field => $info) {
            if (in_array(strtolower($field), $this->sensitive_fields)) {
                return true;
            }
        }
        return false;
    }
}
?>

SSL/TLS Configuration

# Apache SSL configuration for Adminer
<VirtualHost *:443>
    ServerName adminer.example.com
    DocumentRoot /var/www/adminer

    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/adminer.crt
    SSLCertificateKeyFile /etc/ssl/private/adminer.key
    SSLCertificateChainFile /etc/ssl/certs/ca-bundle.crt

    # Strong SSL configuration
    SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder on

    # Security headers
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options nosniff
    Header always set X-Frame-Options DENY
    Header always set X-XSS-Protection "1; mode=block"
    Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"

    # Client certificate authentication (optional)
    SSLVerifyClient require
    SSLVerifyDepth 2
    SSLCACertificateFile /etc/ssl/certs/client-ca.crt

    <Directory /var/www/adminer>
        Options -Indexes
        AllowOverride All
        Require all granted

        # Additional security
        php_admin_value open_basedir "/var/www/adminer:/tmp"
        php_admin_flag allow_url_fopen off
        php_admin_flag allow_url_include off
    </Directory>
</VirtualHost>

Customization

Themes and Styling

<?php
// Custom theme for Adminer
class AdminerCustomTheme extends Adminer {

    function head() {
        echo '<style>
            /* Custom color scheme */
            :root {
                --primary-color: #2c3e50;
                --secondary-color: #3498db;
                --success-color: #27ae60;
                --warning-color: #f39c12;
                --danger-color: #e74c3c;
                --light-bg: #ecf0f1;
                --dark-text: #2c3e50;
            }

            /* Header styling */
            #menu {
                background: var(--primary-color);
                border-bottom: 3px solid var(--secondary-color);
            }

            #menu a {
                color: white;
                text-decoration: none;
                padding: 10px 15px;
                display: inline-block;
                transition: background 0.3s;
            }

            #menu a:hover {
                background: var(--secondary-color);
            }

            /* Table styling */
            table {
                border-collapse: collapse;
                width: 100%;
                margin: 20px 0;
                box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            }

            th {
                background: var(--primary-color);
                color: white;
                padding: 12px;
                text-align: left;
                font-weight: bold;
            }

            td {
                padding: 10px 12px;
                border-bottom: 1px solid #ddd;
            }

            tr:nth-child(even) {
                background: var(--light-bg);
            }

            tr:hover {
                background: #d5dbdb;
            }

            /* Form styling */
            input[type="text"], 
            input[type="password"], 
            input[type="email"], 
            select, 
            textarea {
                padding: 8px 12px;
                border: 2px solid #bdc3c7;
                border-radius: 4px;
                font-size: 14px;
                transition: border-color 0.3s;
            }

            input:focus, select:focus, textarea:focus {
                outline: none;
                border-color: var(--secondary-color);
            }

            /* Button styling */
            input[type="submit"], 
            button {
                background: var(--secondary-color);
                color: white;
                padding: 10px 20px;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 14px;
                transition: background 0.3s;
            }

            input[type="submit"]:hover, 
            button:hover {
                background: #2980b9;
            }

            /* Success/Error messages */
            .message {
                padding: 15px;
                margin: 10px 0;
                border-radius: 4px;
                font-weight: bold;
            }

            .success {
                background: #d4edda;
                color: #155724;
                border: 1px solid #c3e6cb;
            }

            .error {
                background: #f8d7da;
                color: #721c24;
                border: 1px solid #f5c6cb;
            }

            /* SQL editor styling */
            #sql {
                font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
                font-size: 14px;
                line-height: 1.5;
                border: 2px solid #bdc3c7;
                border-radius: 4px;
                padding: 15px;
                background: #f8f9fa;
            }

            /* Responsive design */
            @media (max-width: 768px) {
                table {
                    font-size: 12px;
                }

                th, td {
                    padding: 8px;
                }

                #menu a {
                    padding: 8px 10px;
                    font-size: 14px;
                }
            }
        </style>';

        // Add custom JavaScript
        echo '<script>
            // Auto-refresh for long-running queries
            function autoRefresh() {
                if (document.querySelector(".loading")) {
                    setTimeout(function() {
                        location.reload();
                    }, 5000);
                }
            }

            // Confirm dangerous operations
            function confirmDangerous(action) {
                return confirm("Are you sure you want to " + action + "? This action cannot be undone.");
            }

            // Add confirmation to drop/truncate buttons
            document.addEventListener("DOMContentLoaded", function() {
                var dangerousButtons = document.querySelectorAll("input[value*=\"Drop\"], input[value*=\"Truncate\"]");
                dangerousButtons.forEach(function(button) {
                    button.onclick = function() {
                        return confirmDangerous(this.value.toLowerCase());
                    };
                });

                autoRefresh();
            });
        </script>';
    }

    // Custom page title
    function name() {
        return 'MyApp Database Administration';
    }

    // Custom navigation
    function navigation($missing) {
        echo '<div style="padding: 10px; background: #ecf0f1; margin-bottom: 20px;">';
        echo '<strong>Database:</strong> ' . h(DB);
        echo ' | <strong>Server:</strong> ' . h(SERVER);
        echo ' | <strong>User:</strong> ' . h($this->username());
        echo '</div>';

        return parent::navigation($missing);
    }
}
?>

Plugin Development

<?php
// Custom plugin example: Enhanced JSON editor
class JsonEditorPlugin {

    function __construct() {
        // Register plugin hooks
        add_action('adminer_head', array($this, 'addJsonEditorAssets'));
        add_filter('adminer_edit_input', array($this, 'enhanceJsonFields'));
    }

    function addJsonEditorAssets() {
        echo '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/9.10.2/jsoneditor.min.css">';
        echo '<script src="https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/9.10.2/jsoneditor.min.js"></script>';

        echo '<script>
            function initJsonEditor(textarea) {
                var container = document.createElement("div");
                container.style.height = "300px";
                container.style.width = "100%";

                textarea.parentNode.insertBefore(container, textarea);
                textarea.style.display = "none";

                var options = {
                    mode: "tree",
                    modes: ["code", "tree", "view"],
                    onChange: function() {
                        try {
                            textarea.value = JSON.stringify(editor.get());
                        } catch (e) {
                            // Invalid JSON, keep original value
                        }
                    }
                };

                var editor = new JSONEditor(container, options);

                try {
                    var json = JSON.parse(textarea.value || "{}");
                    editor.set(json);
                } catch (e) {
                    editor.set({});
                }

                return editor;
            }

            document.addEventListener("DOMContentLoaded", function() {
                var jsonFields = document.querySelectorAll("textarea[data-type=\"json\"]");
                jsonFields.forEach(function(field) {
                    initJsonEditor(field);
                });
            });
        </script>';
    }

    function enhanceJsonFields($input, $field, $value) {
        if (in_array(strtolower($field['type']), ['json', 'jsonb'])) {
            return '<textarea name="' . h($field['field']) . '" data-type="json" rows="10" cols="50">' . h($value) . '</textarea>';
        }

        return $input;
    }
}

// Custom plugin: Query performance analyzer
class QueryPerformancePlugin {

    private $query_start_time;
    private $query_log = [];

    function __construct() {
        add_action('adminer_query_start', array($this, 'startQueryTimer'));
        add_action('adminer_query_end', array($this, 'endQueryTimer'));
        add_action('adminer_footer', array($this, 'displayPerformanceStats'));
    }

    function startQueryTimer($query) {
        $this->query_start_time = microtime(true);
    }

    function endQueryTimer($query, $result) {
        $execution_time = microtime(true) - $this->query_start_time;

        $this->query_log[] = [
            'query' => $query,
            'execution_time' => $execution_time,
            'rows_affected' => is_object($result) ? $result->rowCount() : 0,
            'timestamp' => date('H:i:s')
        ];
    }

    function displayPerformanceStats() {
        if (empty($this->query_log)) {
            return;
        }

        echo '<div style="margin-top: 20px; padding: 15px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;">';
        echo '<h3>Query Performance Statistics</h3>';
        echo '<table style="width: 100%; font-size: 12px;">';
        echo '<tr><th>Time</th><th>Query</th><th>Execution Time</th><th>Rows</th></tr>';

        foreach ($this->query_log as $log) {
            $query_preview = strlen($log['query']) > 100 ? 
                substr($log['query'], 0, 100) . '...' : 
                $log['query'];

            $time_class = $log['execution_time'] > 1 ? 'style="color: red; font-weight: bold;"' : '';

            echo '<tr>';
            echo '<td>' . h($log['timestamp']) . '</td>';
            echo '<td title="' . h($log['query']) . '">' . h($query_preview) . '</td>';
            echo '<td ' . $time_class . '>' . number_format($log['execution_time'], 4) . 's</td>';
            echo '<td>' . number_format($log['rows_affected']) . '</td>';
            echo '</tr>';
        }

        echo '</table>';

        $total_time = array_sum(array_column($this->query_log, 'execution_time'));
        $avg_time = $total_time / count($this->query_log);

        echo '<p><strong>Total Queries:</strong> ' . count($this->query_log) . ' | ';
        echo '<strong>Total Time:</strong> ' . number_format($total_time, 4) . 's | ';
        echo '<strong>Average Time:</strong> ' . number_format($avg_time, 4) . 's</p>';
        echo '</div>';
    }
}

// Initialize plugins
new JsonEditorPlugin();
new QueryPerformancePlugin();
?>

Best Practices

Performance Optimization

<?php
// Performance-optimized Adminer configuration
class PerformanceAdminer extends Adminer {

    // Limit result sets to prevent memory issues
    function selectLimit() {
        return 1000; // Default limit for SELECT queries
    }

    // Optimize table listing
    function tablesPrint($tables) {
        // Cache table information
        static $table_cache = [];

        if (empty($table_cache)) {
            foreach ($tables as $table => $type) {
                $table_cache[$table] = [
                    'type' => $type,
                    'rows' => $this->getTableRowCount($table),
                    'size' => $this->getTableSize($table)
                ];
            }
        }

        return parent::tablesPrint($tables);
    }

    private function getTableRowCount($table) {
        static $row_counts = [];

        if (!isset($row_counts[$table])) {
            try {
                $result = $this->connection->query("SELECT COUNT(*) FROM " . idf_escape($table));
                $row_counts[$table] = $result->fetchColumn();
            } catch (Exception $e) {
                $row_counts[$table] = 'N/A';
            }
        }

        return $row_counts[$table];
    }

    private function getTableSize($table) {
        static $table_sizes = [];

        if (!isset($table_sizes[$table])) {
            try {
                $query = "SELECT 
                    ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb
                    FROM information_schema.tables 
                    WHERE table_schema = DATABASE() 
                    AND table_name = " . q($table);

                $result = $this->connection->query($query);
                $table_sizes[$table] = $result->fetchColumn() . ' MB';
            } catch (Exception $e) {
                $table_sizes[$table] = 'N/A';
            }
        }

        return $table_sizes[$table];
    }

    // Optimize query execution
    function query($query, $start = null) {
        // Add query timeout
        if ($this->connection instanceof PDO) {
            $this->connection->setAttribute(PDO::ATTR_TIMEOUT, 30);
        }

        // Log slow queries
        $start_time = microtime(true);
        $result = parent::query($query, $start);
        $execution_time = microtime(true) - $start_time;

        if ($execution_time > 5) { // Log queries taking more than 5 seconds
            $this->logSlowQuery($query, $execution_time);
        }

        return $result;
    }

    private function logSlowQuery($query, $execution_time) {
        $log_entry = [
            'timestamp' => date('Y-m-d H:i:s'),
            'execution_time' => $execution_time,
            'query' => $query,
            'user' => $this->username(),
            'database' => DB
        ];

        file_put_contents(
            '/var/log/adminer/slow_queries.log',
            json_encode($log_entry) . "\n",
            FILE_APPEND | LOCK_EX
        );
    }

    // Memory optimization
    function selectOrderPrint($order, $columns, $indexes) {
        // Limit memory usage for large result sets
        if (ini_get('memory_limit') !== '-1') {
            $memory_limit = $this->parseMemoryLimit(ini_get('memory_limit'));
            $current_usage = memory_get_usage(true);

            if ($current_usage > $memory_limit * 0.8) {
                echo '<div class="message error">Warning: High memory usage detected. Consider limiting your query results.</div>';
            }
        }

        return parent::selectOrderPrint($order, $columns, $indexes);
    }

    private function parseMemoryLimit($limit) {
        $limit = trim($limit);
        $last = strtolower($limit[strlen($limit)-1]);
        $limit = (int) $limit;

        switch($last) {
            case 'g': $limit *= 1024;
            case 'm': $limit *= 1024;
            case 'k': $limit *= 1024;
        }

        return $limit;
    }
}
?>

Security Hardening

<?php
// Security-hardened Adminer configuration
class SecureAdminer extends Adminer {

    // CSRF protection
    private $csrf_token;

    function __construct() {
        session_start();

        // Generate CSRF token
        if (!isset($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        $this->csrf_token = $_SESSION['csrf_token'];

        // Validate CSRF token for POST requests
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            $submitted_token = $_POST['csrf_token'] ?? '';
            if (!hash_equals($this->csrf_token, $submitted_token)) {
                http_response_code(403);
                die('CSRF token validation failed');
            }
        }
    }

    // Add CSRF token to forms
    function head() {
        parent::head();

        echo '<script>
            document.addEventListener("DOMContentLoaded", function() {
                var forms = document.querySelectorAll("form");
                forms.forEach(function(form) {
                    if (form.method.toLowerCase() === "post") {
                        var csrfInput = document.createElement("input");
                        csrfInput.type = "hidden";
                        csrfInput.name = "csrf_token";
                        csrfInput.value = "' . $this->csrf_token . '";
                        form.appendChild(csrfInput);
                    }
                });
            });
        </script>';
    }

    // Input sanitization
    function editInput($table, $field, $attrs, $value) {
        // Sanitize input values
        if (is_string($value)) {
            $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
        }

        return parent::editInput($table, $field, $attrs, $value);
    }

    // SQL injection prevention
    function query($query, $start = null) {
        // Block potentially dangerous queries
        $dangerous_patterns = [
            '/\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i',
            '/\b(UNION\s+SELECT.*FROM\s+information_schema)\b/i',
            '/\b(SELECT.*FROM\s+mysql\.user)\b/i',
            '/\b(BENCHMARK|SLEEP)\s*\(/i'
        ];

        foreach ($dangerous_patterns as $pattern) {
            if (preg_match($pattern, $query)) {
                throw new Exception('Potentially dangerous query blocked');
            }
        }

        return parent::query($query, $start);
    }

    // Session security
    function loginForm() {
        // Regenerate session ID on login form display
        session_regenerate_id(true);

        return parent::loginForm();
    }

    // Secure headers
    function headers() {
        // Security headers
        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: DENY');
        header('X-XSS-Protection: 1; mode=block');
        header('Referrer-Policy: strict-origin-when-cross-origin');
        header('Content-Security-Policy: default-src \'self\'; script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\'');

        // Prevent caching of sensitive pages
        header('Cache-Control: no-cache, no-store, must-revalidate');
        header('Pragma: no-cache');
        header('Expires: 0');

        return parent::headers();
    }

    // Audit logging
    function query($query, $start = null) {
        $result = parent::query($query, $start);

        // Log all database operations
        $this->auditLog([
            'action' => 'query',
            'query' => $query,
            'user' => $this->username(),
            'ip' => $_SERVER['REMOTE_ADDR'],
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
            'timestamp' => date('Y-m-d H:i:s'),
            'database' => DB,
            'success' => $result !== false
        ]);

        return $result;
    }

    private function auditLog($data) {
        $log_file = '/var/log/adminer/audit.log';
        $log_entry = json_encode($data) . "\n";

        file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX);

        // Also log to syslog for centralized logging
        syslog(LOG_INFO, "Adminer: " . json_encode($data));
    }
}

// Use secure configuration
new SecureAdminer;
?>

Maintenance and Monitoring

#!/bin/bash
# Adminer maintenance script

# Configuration
ADMINER_DIR="/var/www/adminer"
LOG_DIR="/var/log/adminer"
BACKUP_DIR="/backups/adminer"
DATE=$(date +%Y%m%d_%H%M%S)

# Create necessary directories
mkdir -p $LOG_DIR $BACKUP_DIR

# Log rotation
find $LOG_DIR -name "*.log" -size +100M -exec gzip {} \;
find $LOG_DIR -name "*.log.gz" -mtime +30 -delete

# Security scan
echo "Running security scan..."
grep -i "error\|warning\|failed" $LOG_DIR/audit.log | tail -20

# Check for suspicious activity
echo "Checking for suspicious activity..."
awk '{print $1}' $LOG_DIR/audit.log | sort | uniq -c | sort -nr | head -10

# Performance monitoring
echo "Performance statistics:"
awk '/execution_time/ {sum+=$2; count++} END {print "Average query time:", sum/count "s"}' $LOG_DIR/slow_queries.log

# Disk space check
echo "Disk space usage:"
df -h $ADMINER_DIR
du -sh $LOG_DIR

# Update check
echo "Checking for Adminer updates..."
CURRENT_VERSION=$(grep -o 'version.*[0-9]\+\.[0-9]\+\.[0-9]\+' $ADMINER_DIR/adminer.php | head -1)
echo "Current version: $CURRENT_VERSION"

# Backup configuration
echo "Creating configuration backup..."
tar -czf $BACKUP_DIR/adminer_config_$DATE.tar.gz $ADMINER_DIR/*.php

# Clean old backups
find $BACKUP_DIR -name "adminer_config_*.tar.gz" -mtime +7 -delete

echo "Maintenance completed at $(date)"

Summary

Adminer is a lightweight, powerful database management tool that provides comprehensive database administration capabilities through a single PHP file. This cheatsheet covers essential features, security practices, and customization options for effective database management.

Key Strengths: - Lightweight: Single PHP file deployment - Multi-Database Support: Works with MySQL, PostgreSQL, SQLite, and more - User-Friendly Interface: Intuitive web-based interface - Extensible: Plugin system for additional functionality - Secure: Built-in security features and customization options

Best Use Cases: - Quick database administration tasks - Shared hosting environments - Development and testing - Remote database management - Lightweight alternative to phpMyAdmin

Important Considerations: - Security configuration is crucial for production use - Performance optimization needed for large databases - Regular updates recommended for security patches - Access control and IP restrictions should be implemented - Audit logging essential for compliance and security monitoring

By following the practices and techniques outlined in this cheatsheet, you can effectively use Adminer to manage databases while maintaining security, performance, and reliability in your database operations.