--- name: "Firebird Database" description: "Development patterns with Firebird database via FireDAC — connection, PSQL, generators, transactions, migrations" --- # Firebird Database — Skill Use this skill when working with Firebird database in Delphi projects via FireDAC. ## When to Use - When configuring FireDAC connection with Firebird - When creating tables, generators, stored procedures, triggers, domains and views - When implementing Repositories with FireDAC + Firebird - When working with transactions, isolation levels and concurrency - When planning schema migrations (versioned scripts) - When optimizing queries and indexes for Firebird ## Firebird Versions | Version | Relevant News | |--------|----------------------| | **2.5** | Trace API, `LIST()` aggregate, Windows Trusted Auth | | **3.0** | Native `BOOLEAN`, `IDENTITY` columns, Packages, UDR (replaces UDF), Window Functions (`OVER`), Encryption | | **4.0** | `DECFLOAT`, `INT128`, `TIME/TIMESTAMP WITH TIME ZONE`, Replication, Batch API, `LATERAL` join | | **5.0** | `WHEN NOT MATCHED BY SOURCE`, Parallel Backup, SQL Security hardening, Profiler | > **Recommendation:** Use Firebird 3.0+ for new projects. Avoid deprecated features like UDFs. ## FireDAC connection with Firebird ### Minimum Configuration ```pascal unit MeuApp.Infra.Database.Connection; interface uses FireDAC.Comp.Client, FireDAC.Phys.FB, // Driver Firebird FireDAC.Phys.FBDef, // Defaults do Firebird FireDAC.Stan.Def, FireDAC.Stan.Pool, FireDAC.DApt; type /// /// Factory de connection Firebird via FireDAC. /// TFirebirdConnectionFactory = class public /// /// Cria e configura uma connection Firebird. /// /// Caminho completo do arquivo .fdb /// User (default: SYSDBA) /// Senha do banco /// Connection FireDAC configurada e aberta class function CreateConnection( const ADatabasePath: string; const AUserName: string = 'SYSDBA'; const APassword: string = 'masterkey' ): TFDConnection; /// /// Cria uma connection via Embedded Server (sem fbserver). /// class function CreateEmbeddedConnection( const ADatabasePath: string ): TFDConnection; end; implementation uses System.SysUtils; class function TFirebirdConnectionFactory.CreateConnection( const ADatabasePath: string; const AUserName: string; const APassword: string): TFDConnection; begin if ADatabasePath.Trim.IsEmpty then raise EArgumentException.Create('ADatabasePath não pode ser vazio'); Result := TFDConnection.Create(nil); try Result.DriverName := 'FB'; Result.Params.Database := ADatabasePath; Result.Params.UserName := AUserName; Result.Params.Password := APassword; { Configurações recomendadas } Result.Params.Values['CharacterSet'] := 'UTF8'; Result.Params.Values['Protocol'] := 'TCPIP'; // Local: 'Local' Result.Params.Values['Server'] := 'localhost'; Result.Params.Values['Port'] := '3050'; Result.Params.Values['SQLDialect'] := '3'; // ALWAYS Dialect 3 Result.Params.Values['PageSize'] := '16384'; // 16KB recomendado { Opções do driver FireDAC } Result.FormatOptions.StrsTrim2Len := True; // Trim CHAR para VARCHAR Result.FetchOptions.Mode := fmAll; // Fetch completo Result.ResourceOptions.AutoReconnect := True; // Reconexão automática Result.TxOptions.Isolation := xiReadCommitted; // Isolation default Result.Connected := True; except Result.Free; raise; end; end; class function TFirebirdConnectionFactory.CreateEmbeddedConnection( const ADatabasePath: string): TFDConnection; begin Result := TFDConnection.Create(nil); try Result.DriverName := 'FB'; Result.Params.Database := ADatabasePath; { Embedded: sem servidor, sem user/password obrigatórios no FB3+ } Result.Params.Values['Protocol'] := 'Local'; Result.Params.Values['CharacterSet'] := 'UTF8'; Result.Params.Values['SQLDialect'] := '3'; Result.Connected := True; except Result.Free; raise; end; end; ``` ### FDPhysFBDriverLink — Configure Client Library ```pascal uses FireDAC.Phys.FBWrapper, FireDAC.Phys.FB; var LDriverLink: TFDPhysFBDriverLink; begin LDriverLink := TFDPhysFBDriverLink.Create(nil); try { Apontar fbclient.dll customizado (32/64-bit) } LDriverLink.VendorLib := 'C:\Firebird\fbclient.dll'; { Embedded: usar fbclient.dll local ao .exe } // LDriverLink.VendorLib := ExtractFilePath(ParamStr(0)) + 'fbclient.dll'; finally { DriverLink geralmente vive por toda a aplicação — criar no DataModule } end; end; ``` ### Connection Pooling ```pascal { No FDManager ou no Connection Definition } FDManager.ConnectionDefs.AddConnectionDef; with FDManager.ConnectionDefs.ConnectionDefByName('FB_POOL') do begin DriverID := 'FB'; Database := 'C:\Data\MeuBanco.fdb'; UserName := 'SYSDBA'; Password := 'masterkey'; Params.Values['CharacterSet'] := 'UTF8'; Params.Values['Pooled'] := 'True'; Params.Values['POOL_MaximumItems'] := '50'; Params.Values['POOL_CleanupTimeout'] := '30000'; Params.Values['POOL_ExpireTimeout'] := '90000'; end; ``` ## Dialects — ALWAYS Dialect 3 | Feature | Dialect 1 | Dialect 3 | |---------|-----------|-----------| | `DATE` | Includes time | Date only (use `TIMESTAMP` for date+time) | | `"Identificadores"` | Syntax error | Allows case-sensitive names with double quotes | | Numerical precision | `DOUBLE PRECISION` | `NUMERIC(18, x)` up to 18 digits | | Recommendation | ❌ Legacy | ✅ **Mandatory for new projects** | > ⚠️ **Rule:** Always `SQLDialect := 3`. Dialect 1 is legacy from InterBase and causes ambiguities with `DATE`. ## Data Types — Firebird Mapping ↔ Delphi | Firebird | Delphi (FireDAC) | Note | |----------|------------------|------------| | `INTEGER` | `ftInteger` / `AsInteger` | 32-bit | | `BIGINT` | `ftLargeint` / `AsLargeInt` | 64-bit | | `SMALLINT` | `ftSmallint` / `AsSmallInt` | 16-bit | | `VARCHAR(N)` | `ftString` / `AsString` | Use with `CHARACTER SET UTF8` | | `CHAR(N)` | `ftFixedChar` | Fill with spaces — prefer `VARCHAR` | | `NUMERIC(P,S)` | `ftBCD` / `AsCurrency` | Monetary values ​​| | `DOUBLE PRECISION`| `ftFloat` / `AsFloat` | Ponto flutuante | | `DATE` | `ftDate` / `AsDateTime` | Date only (Dialect 3) | | `TIME` | `ftTime` / `AsDateTime` | Just in time | | `TIMESTAMP` | `ftDateTime` / `AsDateTime` | Data + Hora | | `BOOLEAN` (FB3+) | `ftBoolean` / `AsBoolean` | `TRUE`/`FALSE` native | | `BLOB SUB_TYPE TEXT` | `ftMemo` / `AsString` | Texto grande (CLOB) | | `BLOB SUB_TYPE 0` | `ftBlob` / `AsBytes` | Binary data | ## Generators (Sequences) ### Create Generator ```sql /* Generator clássico (todas as versões) */ CREATE GENERATOR GEN_CUSTOMER_ID; /* Sequence (Firebird 3+, mais moderno) */ CREATE SEQUENCE SEQ_CUSTOMER_ID; ``` ### Get Next Value in Delphi ```pascal /// /// Obtém o próximo valor de um generator Firebird. /// function GetNextGeneratorValue( AConnection: TFDConnection; const AGeneratorName: string): Int64; var LQuery: TFDQuery; begin LQuery := TFDQuery.Create(nil); try LQuery.Connection := AConnection; LQuery.SQL.Text := 'SELECT GEN_ID(' + AGeneratorName + ', 1) FROM RDB$DATABASE'; LQuery.Open; Result := LQuery.Fields[0].AsLargeInt; finally LQuery.Free; end; end; /// /// Alternativa moderna com NEXT VALUE FOR (Firebird 3+). /// function GetNextSequenceValue( AConnection: TFDConnection; const ASequenceName: string): Int64; var LQuery: TFDQuery; begin LQuery := TFDQuery.Create(nil); try LQuery.Connection := AConnection; LQuery.SQL.Text := 'SELECT NEXT VALUE FOR ' + ASequenceName + ' FROM RDB$DATABASE'; LQuery.Open; Result := LQuery.Fields[0].AsLargeInt; finally LQuery.Free; end; end; ``` ### IDENTITY Columns (Firebird 3+) ```sql /* Auto-increment nativo — dispensa generator manual */ CREATE TABLE customers ( id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL ); /* Para pegar o ID gerado após INSERT: */ INSERT INTO customers (name) VALUES ('João') RETURNING id; ``` ### RETURNING in Delphi (get ID after Insert) ```pascal procedure TFirebirdCustomerRepository.Insert(ACustomer: TCustomer); var LQuery: TFDQuery; begin LQuery := TFDQuery.Create(nil); try LQuery.Connection := FConnection; LQuery.SQL.Text := 'INSERT INTO customers (name, cpf, email) ' + 'VALUES (:name, :cpf, :email) RETURNING id'; LQuery.ParamByName('name').AsString := ACustomer.Name; LQuery.ParamByName('cpf').AsString := ACustomer.Cpf; LQuery.ParamByName('email').AsString := ACustomer.Email; LQuery.Open; { Open, não ExecSQL — pois RETURNING retorna dados } ACustomer.Id := LQuery.Fields[0].AsInteger; finally LQuery.Free; end; end; ``` ## Stored Procedures in Firebird ### Selectable (returns resultset — uses SUSPEND) ```sql CREATE OR ALTER PROCEDURE SP_CUSTOMERS_BY_STATUS ( P_STATUS SMALLINT ) RETURNS ( O_ID INTEGER, O_NAME VARCHAR(100), O_CPF VARCHAR(14), O_STATUS SMALLINT ) AS BEGIN FOR SELECT id, name, cpf, status FROM customers WHERE status = :P_STATUS INTO :O_ID, :O_NAME, :O_CPF, :O_STATUS DO SUSPEND; /* Retorna cada linha (como um cursor) */ END ``` **Call in Delphi (treated as SELECT):** ```pascal LQuery.SQL.Text := 'SELECT * FROM SP_CUSTOMERS_BY_STATUS(:P_STATUS)'; LQuery.ParamByName('P_STATUS').AsSmallInt := Ord(csActive); LQuery.Open; ``` ### Executable (performs action — does not use SUSPEND) ```sql CREATE OR ALTER PROCEDURE SP_DEACTIVATE_CUSTOMER ( P_CUSTOMER_ID INTEGER ) AS BEGIN UPDATE customers SET status = 1 WHERE id = :P_CUSTOMER_ID; END ``` **Call in Delphi:** ```pascal LQuery.SQL.Text := 'EXECUTE PROCEDURE SP_DEACTIVATE_CUSTOMER(:P_CUSTOMER_ID)'; LQuery.ParamByName('P_CUSTOMER_ID').AsInteger := ACustomerId; LQuery.ExecSQL; ``` ## Execute Block (Anonymous SQL with PSQL) ```sql /* Útil para lotes e scripts sem criar procedure permanente */ EXECUTE BLOCK (P_LIMIT INTEGER = :P_LIMIT) RETURNS (O_NAME VARCHAR(100), O_TOTAL INTEGER) AS BEGIN FOR SELECT name, COUNT(*) FROM orders GROUP BY name HAVING COUNT(*) > :P_LIMIT INTO :O_NAME, :O_TOTAL DO SUSPEND; END ``` ## Domains (Reusable Types) ```sql /* Domínios centralizam validações e tipos no schema */ CREATE DOMAIN DM_ID AS INTEGER NOT NULL; CREATE DOMAIN DM_NAME AS VARCHAR(100) NOT NULL; CREATE DOMAIN DM_CPF AS VARCHAR(14); CREATE DOMAIN DM_EMAIL AS VARCHAR(150); CREATE DOMAIN DM_MONEY AS NUMERIC(15, 2) DEFAULT 0; CREATE DOMAIN DM_STATUS AS SMALLINT DEFAULT 0 CHECK (VALUE IN (0, 1, 2)); CREATE DOMAIN DM_BOOLEAN AS SMALLINT DEFAULT 0 CHECK (VALUE IN (0, 1)); /* Firebird 2.5 */ /* Firebird 3+: usar BOOLEAN nativo em vez de DM_BOOLEAN */ CREATE TABLE customers ( id DM_ID, name DM_NAME, cpf DM_CPF, email DM_EMAIL, status DM_STATUS, PRIMARY KEY (id) ); ``` ##Triggers ```sql /* Trigger para auto-increment com Generator */ CREATE OR ALTER TRIGGER TRG_CUSTOMER_BI FOR customers ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.id IS NULL OR NEW.id = 0) THEN NEW.id = GEN_ID(GEN_CUSTOMER_ID, 1); END /* Trigger de auditoria */ CREATE OR ALTER TRIGGER TRG_CUSTOMER_AU FOR customers ACTIVE AFTER UPDATE POSITION 0 AS BEGIN INSERT INTO audit_log (table_name, record_id, action, changed_at) VALUES ('customers', NEW.id, 'UPDATE', CURRENT_TIMESTAMP); END ``` ## Transactions and Isolation Levels ### Isolation Levels in Firebird | Level | FireDAC | Usage | |-------|---------|-----| | **Read Committed** | `xiReadCommitted` | ✅ Standard — reads committed data, without dirty reads | | **Snapshot** (Concurrency) | `xiSnapshot` | Reports — consistent view of START momentum | | **Snapshot Table Stability** | `xiSerializable` | Rare — exclusive lock on table | ### Manual Transaction Control ```pascal /// /// Executa operaction dentro de transaction explícita. /// procedure ExecuteInTransaction(AConnection: TFDConnection; AProc: TProc); begin AConnection.StartTransaction; try AProc; AConnection.Commit; except AConnection.Rollback; raise; end; end; { Uso } ExecuteInTransaction(FConnection, procedure begin FCustomerRepo.Insert(LCustomer); FOrderRepo.Insert(LOrder); FStockRepo.DecreaseStock(LOrder.Items); end ); ``` ### Transaction with Specific Isolation Level ```pascal var LTransaction: TFDTransaction; begin LTransaction := TFDTransaction.Create(nil); try LTransaction.Connection := FConnection; LTransaction.Options.Isolation := xiSnapshot; { Leitura consistente } LTransaction.Options.ReadOnly := True; LTransaction.StartTransaction; try { Queries de relatório aqui — snapshot imutável } LTransaction.Commit; except LTransaction.Rollback; raise; end; finally LTransaction.Free; end; end; ``` ## Event Alerter (Bank Events) ```sql /* No Firebird: */ CREATE OR ALTER TRIGGER TRG_ORDER_NOTIFY FOR orders ACTIVE AFTER INSERT POSITION 0 AS BEGIN POST_EVENT 'NEW_ORDER'; END ``` ```pascal { No Delphi: escutar eventos do banco } uses FireDAC.Phys.FB; // TFDPhysFBEventAlerter var LAlerter: TFDEventAlerter; begin LAlerter := TFDEventAlerter.Create(nil); try LAlerter.Connection := FConnection; LAlerter.Names.Text := 'NEW_ORDER'; LAlerter.Options.Timeout := 0; { Sem timeout — espera indefinidamente } LAlerter.OnAlert := HandleNewOrderEvent; LAlerter.Active := True; finally { Manter vivo enquanto a aplicação rodar — liberação no Destroy } end; end; procedure TMyService.HandleNewOrderEvent(ASender: TFDCustomEventAlerter; const AEventName: string; const AArgument: Variant); begin if AEventName = 'NEW_ORDER' then RefreshOrderList; end; ``` ## Schema Creation — Migration Script ```sql /* migration_001_initial_schema.sql */ /* ===== Domains ===== */ CREATE DOMAIN DM_ID AS INTEGER NOT NULL; CREATE DOMAIN DM_NAME AS VARCHAR(100) NOT NULL; CREATE DOMAIN DM_CPF AS VARCHAR(14); CREATE DOMAIN DM_EMAIL AS VARCHAR(150); CREATE DOMAIN DM_MONEY AS NUMERIC(15,2) DEFAULT 0 NOT NULL; CREATE DOMAIN DM_STATUS AS SMALLINT DEFAULT 0 CHECK (VALUE BETWEEN 0 AND 2); CREATE DOMAIN DM_MEMO AS BLOB SUB_TYPE TEXT SEGMENT SIZE 4096; /* ===== Generators ===== */ CREATE GENERATOR GEN_CUSTOMER_ID; CREATE GENERATOR GEN_PRODUCT_ID; CREATE GENERATOR GEN_ORDER_ID; CREATE GENERATOR GEN_ORDER_ITEM_ID; /* ===== Tables ===== */ CREATE TABLE customers ( id DM_ID, name DM_NAME, cpf DM_CPF, email DM_EMAIL, status DM_STATUS, notes DM_MEMO, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT PK_CUSTOMER PRIMARY KEY (id), CONSTRAINT UQ_CUSTOMER_CPF UNIQUE (cpf) ); CREATE TABLE products ( id DM_ID, name DM_NAME, price DM_MONEY, stock_qty INTEGER DEFAULT 0 NOT NULL, status DM_STATUS, CONSTRAINT PK_PRODUCT PRIMARY KEY (id) ); CREATE TABLE orders ( id DM_ID, customer_id INTEGER NOT NULL, order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, total_amount DM_MONEY, status DM_STATUS, CONSTRAINT PK_ORDER PRIMARY KEY (id), CONSTRAINT FK_ORDER_CUSTOMER FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE RESTRICT ON UPDATE CASCADE ); CREATE TABLE order_items ( id DM_ID, order_id INTEGER NOT NULL, product_id INTEGER NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0), unit_price DM_MONEY, total_price DM_MONEY, CONSTRAINT PK_ORDER_ITEM PRIMARY KEY (id), CONSTRAINT FK_ITEM_ORDER FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE, CONSTRAINT FK_ITEM_PRODUCT FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE RESTRICT ); /* ===== Indices ===== */ CREATE INDEX IDX_CUSTOMER_NAME ON customers (name); CREATE INDEX IDX_ORDER_DATE ON orders (order_date); CREATE INDEX IDX_ORDER_CUSTOMER ON orders (customer_id); CREATE INDEX IDX_ITEM_ORDER ON order_items (order_id); /* ===== Triggers (Auto-Increment) ===== */ SET TERM ^; CREATE TRIGGER TRG_CUSTOMER_BI FOR customers ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.id IS NULL OR NEW.id = 0) THEN NEW.id = GEN_ID(GEN_CUSTOMER_ID, 1); END^ CREATE TRIGGER TRG_PRODUCT_BI FOR products ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.id IS NULL OR NEW.id = 0) THEN NEW.id = GEN_ID(GEN_PRODUCT_ID, 1); END^ CREATE TRIGGER TRG_ORDER_BI FOR orders ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.id IS NULL OR NEW.id = 0) THEN NEW.id = GEN_ID(GEN_ORDER_ID, 1); END^ CREATE TRIGGER TRG_ORDER_ITEM_BI FOR order_items ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW.id IS NULL OR NEW.id = 0) THEN NEW.id = GEN_ID(GEN_ORDER_ITEM_ID, 1); END^ SET TERM ;^ ``` ## Schema migration in Delphi ```pascal /// /// Controle de versão de schema via tabela de migrations. /// procedure EnsureMigrationTable(AConnection: TFDConnection); begin AConnection.ExecSQL( 'CREATE TABLE IF NOT EXISTS (SELECT 1 FROM RDB$RELATIONS ' + 'WHERE RDB$RELATION_NAME = ''SCHEMA_MIGRATIONS'') ' + '/* Alternativa segura: */' ); { No Firebird, CREATE TABLE IF NOT EXISTS não existe nativamente. Verificar via metadata: } if not TableExists(AConnection, 'SCHEMA_MIGRATIONS') then AConnection.ExecSQL( 'CREATE TABLE SCHEMA_MIGRATIONS (' + ' version INTEGER NOT NULL PRIMARY KEY,' + ' description VARCHAR(200),' + ' applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP' + ')' ); end; 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 RDB$RELATIONS ' + 'WHERE RDB$RELATION_NAME = :name AND RDB$SYSTEM_FLAG = 0'; LQuery.ParamByName('name').AsString := ATableName.ToUpper; LQuery.Open; Result := LQuery.Fields[0].AsInteger > 0; finally LQuery.Free; end; end; ``` ## Backup and Restore via GBak (Command Line) ```bash # Backup gbak -b -v -user SYSDBA -password masterkey localhost:C:\Data\MeuBanco.fdb C:\Backup\MeuBanco.fbk # Restore gbak -c -v -page_size 16384 -user SYSDBA -password masterkey C:\Backup\MeuBanco.fbk localhost:C:\Data\MeuBanco_Restored.fdb # Backup via Services API (sem caminho local) gbak -b -se localhost:service_mgr C:\Data\MeuBanco.fdb C:\Backup\MeuBanco.fbk -user SYSDBA -password masterkey ``` ## Firebird Anti-Patterns to Avoid ```pascal // ❌ Dialect 1 — ambiguidade com DATE Result.Params.Values['SQLDialect'] := '1'; // ✅ Dialect 3 — ALWAYS Result.Params.Values['SQLDialect'] := '3'; // ❌ Concatenar SQL — SQL Injection 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; // ❌ Ignorar CharacterSet — problemas com acentos Result.DriverName := 'FB'; Result.Params.Database := APath; Result.Connected := True; // Sem CharacterSet! // ✅ Sempre definir CharacterSet Result.Params.Values['CharacterSet'] := 'UTF8'; // ❌ ExecSQL com RETURNING — not returns nada! LQuery.SQL.Text := 'INSERT INTO ... RETURNING id'; LQuery.ExecSQL; // id PERDIDO! // ✅ Open com RETURNING — returns o resultset LQuery.SQL.Text := 'INSERT INTO ... RETURNING id'; LQuery.Open; // id disponível em Fields[0] ACustomer.Id := LQuery.Fields[0].AsInteger; // ❌ Criar banco sem Page Size adequado CREATE DATABASE '...' PAGE_SIZE 4096; // Muito pequeno para tabelas grandes // ✅ Page Size otimizado CREATE DATABASE '...' PAGE_SIZE 16384 DEFAULT CHARACTER SET UTF8; // ❌ Not tratar deadlocks LQuery.ExecSQL; // pode dar deadlock em concorrência // ✅ Tratar deadlocks com retry try LQuery.ExecSQL; except on E: EFDDBEngineException do begin if E.Kind = ekRecordLocked then raise EConflictException.Create('Registro bloqueado por outra transação') else raise; end; end; ``` ## Packages (Firebird 3+) ```sql /* Packages agrupam procedures e functions relacionadas */ /* Header (interface) */ CREATE OR ALTER PACKAGE PKG_CUSTOMER AS BEGIN PROCEDURE DEACTIVATE(P_ID INTEGER); FUNCTION GET_FULL_NAME(P_ID INTEGER) RETURNS VARCHAR(200); END /* Body (implementation) */ CREATE OR ALTER PACKAGE BODY PKG_CUSTOMER AS BEGIN PROCEDURE DEACTIVATE(P_ID INTEGER) AS BEGIN UPDATE customers SET status = 1 WHERE id = :P_ID; END FUNCTION GET_FULL_NAME(P_ID INTEGER) RETURNS VARCHAR(200) AS DECLARE VARIABLE V_NAME VARCHAR(200); BEGIN SELECT name FROM customers WHERE id = :P_ID INTO :V_NAME; RETURN V_NAME; END END ``` **Call in Delphi:** ```pascal LQuery.SQL.Text := 'EXECUTE PROCEDURE PKG_CUSTOMER.DEACTIVATE(:ID)'; LQuery.ParamByName('ID').AsInteger := ACustomerId; LQuery.ExecSQL; { Function } LQuery.SQL.Text := 'SELECT PKG_CUSTOMER.GET_FULL_NAME(:ID) FROM RDB$DATABASE'; LQuery.ParamByName('ID').AsInteger := ACustomerId; LQuery.Open; LFullName := LQuery.Fields[0].AsString; ``` ## Integration Testing with Firebird Embedded ```pascal [TestFixture] TCustomerRepositoryFirebirdTest = class private FConnection: TFDConnection; FDriverLink: TFDPhysFBDriverLink; FRepository: ICustomerRepository; FTestDbPath: string; public [Setup] procedure Setup; [TearDown] procedure TearDown; [Test] procedure Insert_ValidCustomer_ShouldReturnGeneratedId; end; procedure TCustomerRepositoryFirebirdTest.Setup; begin FTestDbPath := TPath.Combine(TPath.GetTempPath, 'test_' + TGUID.NewGuid.ToString + '.fdb'); { Configurar driver embedded } FDriverLink := TFDPhysFBDriverLink.Create(nil); FDriverLink.VendorLib := 'fbclient.dll'; { Embedded no path da aplicação } { Criar banco de teste } FConnection := TFDConnection.Create(nil); FConnection.DriverName := 'FB'; FConnection.Params.Database := FTestDbPath; FConnection.Params.UserName := 'SYSDBA'; FConnection.Params.Password := 'masterkey'; FConnection.Params.Values['Protocol'] := 'Local'; FConnection.Params.Values['CharacterSet'] := 'UTF8'; FConnection.Params.Values['CreateDatabase'] := 'Yes'; { Cria o .fdb automaticamente } FConnection.Connected := True; { Criar schema de teste } FConnection.ExecSQL('CREATE TABLE customers (id INTEGER PRIMARY KEY, name VARCHAR(100))'); FConnection.ExecSQL('CREATE GENERATOR GEN_CUSTOMER_ID'); FRepository := TFirebirdCustomerRepository.Create(FConnection); end; procedure TCustomerRepositoryFirebirdTest.TearDown; begin FConnection.Connected := False; FConnection.Free; FDriverLink.Free; { Limpar banco temporário } if TFile.Exists(FTestDbPath) then TFile.Delete(FTestDbPath); end; ``` ## Firebird Checklist - [ ] Dialect 3 configured (`SQLDialect := '3'`)? - [ ] CharacterSet UTF8 defined? - [ ] Page Size ≥ 8192 (ideal: 16384)? - [ ] Parameterized queries (without string concatenation)? - [ ] Generators/Sequences for auto-increment with BI trigger? - [ ] `RETURNING` with `Open` (not `ExecSQL`)? - [ ] Explicit transactions for compound operations? - [ ] Deadlocks treated with `EFDDBEngineException.Kind = ekRecordLocked`? - [ ] Correct FBClient.dll (32/64-bit) configured in VendorLib? - [ ] Indexes created for columns used in WHERE and JOIN? - [ ] Foreign Keys with appropriate `ON DELETE`/`ON UPDATE`? - [ ] Regular backup via `gbak`? - [ ] Versioned migration scripts?