--- name: "MySQL Database" description: "Development patterns with MySQL/MariaDB via FireDAC — connection, stored procedures, AUTO_INCREMENT, JSON, triggers, replication, migrations" --- # MySQL Database — Skill Use this skill when working with MySQL or MariaDB databases in Delphi projects via FireDAC. ## When to Use - When configuring FireDAC connection with MySQL or MariaDB - When creating tables, stored procedures, functions, triggers and views - When implementing Repositories with FireDAC + MySQL - When working with native JSON (MySQL 5.7+), Full-Text Search, Partitioning - When planning schema migrations (versioned scripts) - When developing web applications with MySQL backend ## MySQL Versions | Version | Relevant News | |--------|----------------------| | **5.7** | Native JSON, Generated Columns, `sys` schema, Group Replication | | **8.0** | Recursive CTEs, Window Functions, `DEFAULT (expr)`, Roles, `INVISIBLE` indexes, `NOWAIT`/`SKIP LOCKED` | | **8.4 LTS** | LTS release, Firewall improvements, Plugin improvements | | **9.0+** | Vector type, JavaScript stored programs (preview) | ### MariaDB | Version | Relevant News | |--------|----------------------| | **10.2** | Recursive CTEs, Window Functions, `DEFAULT (expr)` | | **10.3** | `INVISIBLE` columns, `INTERSECT`/`EXCEPT`, Sequences | | **10.5** | `INET6` type, `JSON_TABLE`, S3 storage engine | | **11.0+** | Release Calendar, UUID v7, `VECTOR` type | > **Recommendation:** Use MySQL 8.0+ or ​​MariaDB 10.5+ for new projects. ## FireDAC connection with MySQL ### Minimum Configuration ```pascal unit MeuApp.Infra.Database.MySQL.Connection; interface uses FireDAC.Comp.Client, FireDAC.Phys.MySQL, // Driver MySQL FireDAC.Phys.MySQLDef, // Defaults do MySQL FireDAC.Stan.Def, FireDAC.DApt; type /// /// Factory de connection MySQL via FireDAC. /// TMySQLConnectionFactory = class public class function CreateConnection( const AServer: string; const ADatabase: string; const AUserName: string = 'root'; const APassword: string = ''; APort: Integer = 3306 ): TFDConnection; end; implementation uses System.SysUtils; class function TMySQLConnectionFactory.CreateConnection( const AServer, ADatabase, AUserName, APassword: string; APort: Integer): TFDConnection; begin if ADatabase.Trim.IsEmpty then raise EArgumentException.Create('ADatabase não pode ser vazio'); Result := TFDConnection.Create(nil); try Result.DriverName := 'MySQL'; Result.Params.Values['Server'] := AServer; Result.Params.Values['Port'] := APort.ToString; Result.Params.Database := ADatabase; Result.Params.UserName := AUserName; Result.Params.Password := APassword; { Configurações recomendadas } Result.Params.Values['CharacterSet'] := 'utf8mb4'; // ALWAYS utf8mb4 (suporta emoji/4-byte) { Opções do driver FireDAC } Result.FormatOptions.StrsTrim2Len := True; Result.FetchOptions.Mode := fmAll; Result.ResourceOptions.AutoReconnect := True; Result.TxOptions.Isolation := xiReadCommitted; Result.Connected := True; except Result.Free; raise; end; end; ``` ### FDPhysMySQLDriverLink — Configure Client Library ```pascal uses FireDAC.Phys.MySQLWrapper, FireDAC.Phys.MySQL; var LDriverLink: TFDPhysMySQLDriverLink; begin LDriverLink := TFDPhysMySQLDriverLink.Create(nil); try { Para MySQL 8.x: libmysql.dll } LDriverLink.VendorLib := 'C:\MySQL\lib\libmysql.dll'; { Para MariaDB: libmariadb.dll } // LDriverLink.VendorLib := 'C:\MariaDB\lib\libmariadb.dll'; finally { DriverLink vive por toda a aplicação — criar no DataModule } end; end; ``` > **ATTENTION:** `utf8` in MySQL is only 3 bytes (does not support emoji 🎉). **always use `utf8mb4`** for full charset. MySQL's `utf8` is an alias for `utf8mb3`. ### Connection Pooling ```pascal { Via FDManager } with FDManager.ConnectionDefs.AddConnectionDef do begin Name := 'MySQL_Pool'; DriverID := 'MySQL'; Params.Values['Server'] := 'localhost'; Params.Values['Port'] := '3306'; Params.Values['Database'] := 'meubanco'; Params.Values['User_Name'] := 'root'; Params.Values['Password'] := 'senha'; Params.Values['CharacterSet'] := 'utf8mb4'; Params.Values['Pooled'] := 'True'; Params.Values['POOL_MaximumItems'] := '50'; Params.Values['POOL_CleanupTimeout'] := '30000'; end; ``` ### SSL/TLS ```pascal Result.Params.Values['SSL_ca'] := '/path/to/ca-cert.pem'; Result.Params.Values['SSL_cert'] := '/path/to/client-cert.pem'; Result.Params.Values['SSL_key'] := '/path/to/client-key.pem'; ``` ## Data Types — MySQL Mapping ↔ Delphi | MySQL | Delphi (FireDAC) | Note | |-------|------------------|------------| | `INT` / `INTEGER` | `ftInteger` / `AsInteger` | 32-bit signed | | `BIGINT` | `ftLargeint` / `AsLargeInt` | 64-bit | | `SMALLINT` | `ftSmallint` / `AsSmallInt` | 16-bit | | `TINYINT` | `ftSmallint` / `AsSmallInt` | 8-bit (`ftByte` does not exist) | | `TINYINT(1)` | `ftBoolean` / `AsBoolean` | MySQL Convention for Boolean | | `VARCHAR(N)` | `ftString` / `AsString` | Limited text | | `TEXT` | `ftMemo` / `AsString` | Long text (up to 64KB) | | `LONGTEXT` | `ftMemo` / `AsString` | Very long text (up to 4GB) | | `DECIMAL(P,S)` | `ftBCD` / `AsCurrency` | Monetary values ​​| | `DOUBLE` | `ftFloat` / `AsFloat` | Ponto flutuante | | `FLOAT` | `ftSingle` / `AsSingle` | 32-bit float | | `DATE` | `ftDate` / `AsDateTime` | Date only | | `TIME` | `ftTime` / `AsDateTime` | Just in time | | `DATETIME` | `ftDateTime` / `AsDateTime` | Date + Time (without timezone) | | `TIMESTAMP` | `ftDateTime` / `AsDateTime` | Data + Hora (auto-update, UTC) | | `BOOLEAN` / `BOOL` | `ftBoolean` / `AsBoolean` | Alias ​​for `TINYINT(1)` | | `JSON` | `ftMemo` / `AsString` | Native JSON (MySQL 5.7+) | | `BLOB` | `ftBlob` / `AsBytes` | Binary data | | `LONGBLOB` | `ftBlob` / `AsBytes` | Large binary (up to 4GB) | | `ENUM(...)` | `ftString` / `AsString` | Up to 65535 values ​​| | `SET(...)` | `ftString` / `AsString` | Combination of values ​​| | `CHAR(36)` | `ftString` / `AsString` | UUID as string | ## AUTO_INCREMENT ### Table with AUTO_INCREMENT ```sql CREATE TABLE customers ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(150) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ``` ### Get the ID Generated in Delphi ```pascal /// /// Insere customer e obtém o id gerado pelo AUTO_INCREMENT. /// MySQL NÃO suporta RETURNING — usar LAST_INSERT_ID(). /// procedure TMySQLCustomerRepository.Insert(ACustomer: TCustomer); var LQuery: TFDQuery; begin LQuery := TFDQuery.Create(nil); try LQuery.Connection := FConnection; { Método 1: Duas queries (mais seguro e portável) } LQuery.SQL.Text := 'INSERT INTO customers (name, cpf, email, status) ' + 'VALUES (:name, :cpf, :email, :status)'; LQuery.ParamByName('name').AsString := ACustomer.Name; LQuery.ParamByName('cpf').AsString := ACustomer.Cpf; LQuery.ParamByName('email').AsString := ACustomer.Email; LQuery.ParamByName('status').AsSmallInt := Ord(ACustomer.Status); LQuery.ExecSQL; { Obter LAST_INSERT_ID() } LQuery.SQL.Text := 'SELECT LAST_INSERT_ID() AS new_id'; LQuery.Open; ACustomer.Id := LQuery.FieldByName('new_id').AsInteger; { Método 2: Via propriedade FireDAC (mais direto) } // ACustomer.Id := FConnection.GetLastAutoGenValue(''); finally LQuery.Free; end; end; ``` > **⚠️ ATTENTION:** MySQL **DOES NOT** support `RETURNING`. Use `LAST_INSERT_ID()` or `FConnection.GetLastAutoGenValue('')`. This is a **critical difference** compared to Firebird and PostgreSQL. ## UPSERT — INSERT ... ON DUPLICATE KEY UPDATE ```sql -- Inserir ou atualizar se a PK/UNIQUE já existir INSERT INTO customers (cpf, name, email, status) VALUES (:cpf, :name, :email, :status) ON DUPLICATE KEY UPDATE name = VALUES(name), email = VALUES(email), status = VALUES(status); -- MySQL 8.0.19+: Alias com AS INSERT INTO customers (cpf, name, email, status) VALUES (:cpf, :name, :email, :status) AS new_data ON DUPLICATE KEY UPDATE name = new_data.name, email = new_data.email, status = new_data.status; ``` **In Delphi:** ```pascal procedure TMySQLCustomerRepository.Upsert(ACustomer: TCustomer); var LQuery: TFDQuery; begin LQuery := TFDQuery.Create(nil); try LQuery.Connection := FConnection; LQuery.SQL.Text := 'INSERT INTO customers (cpf, name, email, status) ' + 'VALUES (:cpf, :name, :email, :status) ' + 'ON DUPLICATE KEY UPDATE ' + ' name = VALUES(name), email = VALUES(email), status = VALUES(status)'; LQuery.ParamByName('cpf').AsString := ACustomer.Cpf; LQuery.ParamByName('name').AsString := ACustomer.Name; LQuery.ParamByName('email').AsString := ACustomer.Email; LQuery.ParamByName('status').AsSmallInt := Ord(ACustomer.Status); LQuery.ExecSQL; { Obter ID (seja insert ou update) } LQuery.SQL.Text := 'SELECT LAST_INSERT_ID() AS new_id'; LQuery.Open; ACustomer.Id := LQuery.FieldByName('new_id').AsInteger; finally LQuery.Free; end; end; ``` ## Native JSON (MySQL 5.7+) ### Storage and Query ```sql -- Tabela com coluna JSON CREATE TABLE customer_settings ( customer_id INT NOT NULL REFERENCES customers(id), settings JSON NOT NULL, PRIMARY KEY (customer_id) ); -- Inserir JSON INSERT INTO customer_settings (customer_id, settings) VALUES (1, '{"theme": "dark", "language": "pt-BR", "notifications": true}'); -- Consultar campo específico (operador ->>) SELECT JSON_UNQUOTE(JSON_EXTRACT(settings, '$.theme')) AS theme FROM customer_settings WHERE customer_id = 1; -- Sintaxe curta com ->> SELECT settings->>'$.theme' AS theme FROM customer_settings WHERE customer_id = 1; -- Filtrar por valor JSON SELECT * FROM customer_settings WHERE JSON_CONTAINS(settings, '"dark"', '$.theme'); -- Índice virtual para busca em JSON (Generated Column + Index) ALTER TABLE customer_settings ADD COLUMN theme VARCHAR(50) GENERATED ALWAYS AS (settings->>'$.theme') VIRTUAL, ADD INDEX idx_theme (theme); ``` **In Delphi:** ```pascal { Inserir JSON } LQuery.SQL.Text := 'INSERT INTO customer_settings (customer_id, settings) ' + 'VALUES (:customer_id, :settings)'; LQuery.ParamByName('customer_id').AsInteger := ACustomerId; LQuery.ParamByName('settings').AsString := AJsonString; LQuery.ExecSQL; { Ler campo JSON } LQuery.SQL.Text := 'SELECT settings->>''$.theme'' AS theme ' + 'FROM customer_settings WHERE customer_id = :id'; LQuery.ParamByName('id').AsInteger := ACustomerId; LQuery.Open; LTheme := LQuery.FieldByName('theme').AsString; ``` ## Full-Text Search (InnoDB) ```sql -- Índice FULLTEXT (InnoDB, MyISAM) ALTER TABLE products ADD FULLTEXT INDEX ft_product_search (name, description); -- Busca Natural Language SELECT *, MATCH(name, description) AGAINST('camisa azul' IN NATURAL LANGUAGE MODE) AS relevance FROM products WHERE MATCH(name, description) AGAINST('camisa azul' IN NATURAL LANGUAGE MODE) ORDER BY relevance DESC; -- Busca Boolean Mode (mais controle) SELECT * FROM products WHERE MATCH(name, description) AGAINST('+camisa +azul -infantil' IN BOOLEAN MODE); ``` ## Stored Procedures and Functions ```sql -- Procedure (equivale a Executable no Firebird) DELIMITER // CREATE PROCEDURE sp_deactivate_customer(IN p_id INT) BEGIN UPDATE customers SET status = 1, updated_at = NOW() WHERE id = p_id; IF ROW_COUNT() = 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Customer not found'; END IF; END // DELIMITER ; -- Function escalar DELIMITER // CREATE FUNCTION fn_customer_full_name(p_id INT) RETURNS VARCHAR(200) READS SQL DATA BEGIN DECLARE v_name VARCHAR(200); SELECT name INTO v_name FROM customers WHERE id = p_id; IF v_name IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Customer not found'; END IF; RETURN v_name; END // DELIMITER ; ``` **Call in Delphi:** ```pascal { Procedure } LQuery.SQL.Text := 'CALL sp_deactivate_customer(:p_id)'; LQuery.ParamByName('p_id').AsInteger := ACustomerId; LQuery.ExecSQL; { Function escalar } LQuery.SQL.Text := 'SELECT fn_customer_full_name(:p_id) AS full_name'; LQuery.ParamByName('p_id').AsInteger := ACustomerId; LQuery.Open; LFullName := LQuery.FieldByName('full_name').AsString; ``` > **Note:** MySQL Procedures are called with `CALL`, Functions with `SELECT`. Procedures can return result sets via `SELECT` inside the body. ## ENUM and SET ```sql -- ENUM: valor único de uma lista CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, status ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled') NOT NULL DEFAULT 'pending', priority ENUM('low', 'medium', 'high') NOT NULL DEFAULT 'medium' ); -- SET: múltiplos valores de uma lista CREATE TABLE products ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), tags SET('new', 'sale', 'featured', 'limited') NOT NULL DEFAULT '' ); -- Inserir SET INSERT INTO products (name, tags) VALUES ('Camisa', 'new,featured'); ``` **In Delphi (map to Pascal enum):** ```pascal type TOrderStatus = (osPending, osProcessing, osShipped, osDelivered, osCancelled); const ORDER_STATUS_NAMES: array[TOrderStatus] of string = ( 'pending', 'processing', 'shipped', 'delivered', 'cancelled' ); { Ler do banco } LOrder.Status := StringToOrderStatus(LQuery.FieldByName('status').AsString); { Gravar no banco } LQuery.ParamByName('status').AsString := ORDER_STATUS_NAMES[AOrder.Status]; ``` ##Triggers ```sql DELIMITER // -- Trigger BEFORE INSERT para validação CREATE TRIGGER trg_customer_before_insert BEFORE INSERT ON customers FOR EACH ROW BEGIN SET NEW.created_at = NOW(); SET NEW.updated_at = NOW(); IF NEW.name = '' OR NEW.name IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Customer name cannot be empty'; END IF; END // -- Trigger BEFORE UPDATE para atualizar timestamp CREATE TRIGGER trg_customer_before_update BEFORE UPDATE ON customers FOR EACH ROW BEGIN SET NEW.updated_at = NOW(); END // DELIMITER ; ``` ## Transactions and Isolation Levels ### Isolation Levels in MySQL | Level | FireDAC | Usage | |-------|---------|-----| | **Read Uncommitted** | `xiDirtyRead` | Almost never — reads uncommitted data | | **Read Committed** | `xiReadCommitted` | ✅ Recommended pattern | | **Repeatable Read** | `xiRepeatableRead` | InnoDB default — snapshot at start of tx | | **Serializable** | `xiSerializable` | Maximum consistency (implicit locks) | > **Note:** InnoDB's default isolation is `REPEATABLE READ`, unlike Firebird/PostgreSQL which use `READ COMMITTED`. ### Explicit Transaction ```pascal procedure ExecuteInTransaction(AConnection: TFDConnection; AProc: TProc); begin AConnection.StartTransaction; try AProc; AConnection.Commit; except AConnection.Rollback; raise; end; end; ``` ### SAVEPOINT ```pascal FConnection.StartTransaction; try FCustomerRepo.Insert(LCustomer); FConnection.ExecSQL('SAVEPOINT before_order'); try FOrderRepo.Insert(LOrder); except FConnection.ExecSQL('ROLLBACK TO SAVEPOINT before_order'); end; FConnection.Commit; except FConnection.Rollback; raise; end; ``` ## InnoDB vs MyISAM | Feature | InnoDB | MyISAM | |---------|--------|--------| | Transactions | ✅ Yes | ❌ No | | Foreign Keys | ✅ Yes | ❌ No | | Row-level Locking | ✅ Yes | ❌ Table-level | | Full-Text Search | ✅ Yes (5.6+) | ✅ Yes | | Crash Recovery | ✅ Yes | ❌ No | > **Rule:** Use **always InnoDB** (`ENGINE=InnoDB`). Never MyISAM in new projects. ## Schema Creation — Migration Script ```sql /* migration_001_initial_schema.sql */ /* ===== Tabelas ===== */ CREATE TABLE IF NOT EXISTS customers ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, cpf VARCHAR(14) UNIQUE, email VARCHAR(150), status TINYINT NOT NULL DEFAULT 0 COMMENT '0=active, 1=inactive, 2=suspended', notes TEXT, metadata JSON, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_customer_name (name), INDEX idx_customer_cpf (cpf) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS products ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, price DECIMAL(15, 2) NOT NULL DEFAULT 0.00, stock_qty INT NOT NULL DEFAULT 0, description TEXT, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FULLTEXT INDEX ft_product (name, description) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS orders ( id INT AUTO_INCREMENT PRIMARY KEY, customer_id INT NOT NULL, order_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, total_amount DECIMAL(15, 2) NOT NULL DEFAULT 0.00, status ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled') NOT NULL DEFAULT 'pending', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_order_customer (customer_id), INDEX idx_order_date (order_date), FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE IF NOT EXISTS order_items ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, unit_price DECIMAL(15, 2) NOT NULL, total_price DECIMAL(15, 2) GENERATED ALWAYS AS (quantity * unit_price) STORED, FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /* ===== Triggers ===== */ DELIMITER // CREATE TRIGGER trg_customer_validate BEFORE INSERT ON customers FOR EACH ROW BEGIN IF NEW.name = '' OR NEW.name IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Name cannot be empty'; END IF; END // DELIMITER ; /* ===== Procedures ===== */ DELIMITER // CREATE PROCEDURE sp_deactivate_customer(IN p_id INT) BEGIN UPDATE customers SET status = 1 WHERE id = p_id; IF ROW_COUNT() = 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Customer not found'; END IF; END // DELIMITER ; ``` ## Schema migration in Delphi ```pascal /// /// Verifica se uma tabela existe no MySQL. /// function TableExists(AConnection: TFDConnection; const ATableName: string): Boolean; var LQuery: TFDQuery; begin LQuery := TFDQuery.Create(nil); try LQuery.Connection := AConnection; LQuery.SQL.Text := 'SELECT COUNT(*) FROM information_schema.tables ' + 'WHERE table_schema = DATABASE() AND table_name = :name'; LQuery.ParamByName('name').AsString := ATableName; LQuery.Open; Result := LQuery.Fields[0].AsInteger > 0; finally LQuery.Free; end; end; /// /// Verifica se uma coluna existe em uma tabela. /// function ColumnExists(AConnection: TFDConnection; const ATableName, AColumnName: string): Boolean; var LQuery: TFDQuery; begin LQuery := TFDQuery.Create(nil); try LQuery.Connection := AConnection; LQuery.SQL.Text := 'SELECT COUNT(*) FROM information_schema.columns ' + 'WHERE table_schema = DATABASE() AND table_name = :table AND column_name = :col'; LQuery.ParamByName('table').AsString := ATableName; LQuery.ParamByName('col').AsString := AColumnName; LQuery.Open; Result := LQuery.Fields[0].AsInteger > 0; finally LQuery.Free; end; end; ``` ## MySQL Error Handling ```pascal except on E: EFDDBEngineException do begin case E.Kind of ekUKViolated: raise EDuplicateException.Create('Registro duplicado: ' + E.Message); ekFKViolated: raise EDependencyException.Create('Violação de FK: ' + E.Message); ekRecordLocked: raise EConflictException.Create('Registro bloqueado — deadlock'); ekServerGone: raise EConnectionLostException.Create('Conexão com MySQL perdida'); else raise; end; end; end; { Verificar código de erro MySQL específico } except on E: EFDDBEngineException do begin { Códigos de erro MySQL comuns: } { 1062 = ER_DUP_ENTRY (duplicate key) } { 1451 = ER_ROW_IS_REFERENCED_2 (FK restrict) } { 1452 = ER_NO_REFERENCED_ROW_2 (FK no parent) } { 1213 = ER_LOCK_DEADLOCK } { 1205 = ER_LOCK_WAIT_TIMEOUT } { 2006 = CR_SERVER_GONE_ERROR } { 2013 = CR_SERVER_LOST } if E.Errors[0].ErrorCode = 1062 then raise EDuplicateException.Create('Valor duplicado') else raise; end; end; ``` ## CTEs and Window Functions (MySQL 8.0+) ```sql -- CTE (MySQL 8.0+) WITH active_customers AS ( SELECT id, name, email FROM customers WHERE status = 0 ) SELECT ac.name, COUNT(o.id) AS total_orders FROM active_customers ac LEFT JOIN orders o ON o.customer_id = ac.id GROUP BY ac.id, ac.name; -- CTE Recursiva (hierarquia) WITH RECURSIVE category_tree AS ( SELECT id, name, parent_id, 0 AS level FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.name, c.parent_id, ct.level + 1 FROM categories c JOIN category_tree ct ON c.parent_id = ct.id ) SELECT * FROM category_tree ORDER BY level, name; -- Window Functions (MySQL 8.0+) SELECT name, total_spent, RANK() OVER (ORDER BY total_spent DESC) AS ranking, ROW_NUMBER() OVER (ORDER BY total_spent DESC) AS row_num FROM ( SELECT c.name, SUM(o.total_amount) AS total_spent FROM customers c JOIN orders o ON o.customer_id = c.id GROUP BY c.id, c.name ) ranked; ``` ## UUID as Primary Key ```sql -- Gerar UUID no MySQL CREATE TABLE sessions ( id CHAR(36) NOT NULL DEFAULT (UUID()) PRIMARY KEY, user_id INT NOT NULL, token VARCHAR(255), INDEX idx_session_user (user_id) ) ENGINE=InnoDB; -- MySQL 8.0+: UUID() como DEFAULT -- MySQL < 8.0: gerar no Delphi e enviar como parâmetro ``` **In Delphi:** ```pascal uses System.SysUtils; { Gerar UUID no Delphi para MySQL < 8.0 } LQuery.ParamByName('id').AsString := TGUID.NewGuid.ToString; ``` ## Partitioning ```sql -- Particionamento por RANGE CREATE TABLE orders ( id INT AUTO_INCREMENT, customer_id INT NOT NULL, order_date DATE NOT NULL, total DECIMAL(15,2), PRIMARY KEY (id, order_date) ) ENGINE=InnoDB PARTITION BY RANGE (YEAR(order_date)) ( PARTITION p2024 VALUES LESS THAN (2025), PARTITION p2025 VALUES LESS THAN (2026), PARTITION p2026 VALUES LESS THAN (2027), PARTITION pmax VALUES LESS THAN MAXVALUE ); ``` ## Key Differences: MySQL vs Firebird vs PostgreSQL | Feature | MySQL | Firebird | PostgreSQL | |---------|-------|----------|------------| | Auto-increment | `AUTO_INCREMENT` | Generator + BI Trigger | `SERIAL` / `IDENTITY` | | Get generated ID | `LAST_INSERT_ID()` | `RETURNING id` | `RETURNING id` | | UPSERT | `ON DUPLICATE KEY UPDATE` | Non-native (partial FB5) | `ON CONFLICT` | | JSON | `JSON` (5.7+) | Non-native | `JSONB` (indexable) | | Full-Text Search | `FULLTEXT` index | Non-native | `tsvector` | | ENUM | `ENUM(...)` native | Domain + CHECK | `CREATE TYPE` | | Embedded | No | Yes (fbclient.dll) | No | | Engine Choice | InnoDB, MyISAM, etc. | Single engine | Single engine | | Recommended Charset | `utf8mb4` | `UTF8` | `UTF8` | | FireDAC Driver | `MySQL` | `FB` | `PG` | | Client Library | `libmysql.dll` | `fbclient.dll` | `libpq.dll` | | Default Isolation | Repeatable Read | Read Committed | Read Committed | | StoredProcs | `CALL sp()` | `EXECUTE PROCEDURE` | `CALL sp()` (PG 11+) | | Windows Functions | 8.0+ | 3.0+ (basic) | Ample | | CTEs | 8.0+ | 3.0+ | All versions | ## MySQL Anti-Patterns to Avoid ```pascal // ❌ Concatenar SQL LQuery.SQL.Text := 'SELECT * FROM customers WHERE name = ''' + AName + ''''; // ✅ Parâmetros parametrizados LQuery.SQL.Text := 'SELECT * FROM customers WHERE name = :name'; LQuery.ParamByName('name').AsString := AName; // ❌ Usar utf8 (3 bytes, not suporta emoji) Result.Params.Values['CharacterSet'] := 'utf8'; // ✅ Usar utf8mb4 (4 bytes, suporte completo) Result.Params.Values['CharacterSet'] := 'utf8mb4'; // ❌ Tentar usar RETURNING (not existe no MySQL!) LQuery.SQL.Text := 'INSERT INTO ... RETURNING id'; // ✅ Usar LAST_INSERT_ID() LQuery.ExecSQL; LQuery.SQL.Text := 'SELECT LAST_INSERT_ID()'; LQuery.Open; // ❌ Usar MyISAM para tabelas novas CREATE TABLE t (...) ENGINE=MyISAM; // ✅ Usar InnoDB sempre CREATE TABLE t (...) ENGINE=InnoDB; // ❌ SELECT * sem LIMIT LQuery.SQL.Text := 'SELECT * FROM orders'; // ✅ LIMIT para paginaction LQuery.SQL.Text := 'SELECT id, customer_id, total FROM orders LIMIT :limit OFFSET :offset'; // ❌ Ignorar índices em colunas de WHERE/JOIN // ✅ Criar índices para colunas usadas em filtros ``` ## MySQL Checklist - [ ] Driver `MySQL` configured on FireDAC? - [ ] `CharacterSet := 'utf8mb4'` (NOT `utf8`)? - [ ] `libmysql.dll` (32/64-bit) in PATH or `VendorLib`? - [ ] Tables created with `ENGINE=InnoDB`? - [ ] `AUTO_INCREMENT` in PKs with `LAST_INSERT_ID()` in Delphi? - [ ] Parameterized queries (without concatenation)? - [ ] Explicit transactions for compound operations? - [ ] Errors handled via `EFDDBEngineException.Kind`? - [ ] Indexes created for columns in WHERE and JOIN? - [ ] Foreign Keys with appropriate `ON DELETE`/`ON UPDATE`? - [ ] `COLLATE utf8mb4_unicode_ci` in the tables for correct comparison? - [ ] `information_schema` to check metadata?