SimpleORM

Complete ORM for Delphi — Entity mapping, CRUD, validation, pagination, Horse integration

Repository: github.com/academiadocodigo/SimpleORM

Installation

Via Boss (recommended)

boss install academiadocodigo/SimpleORM

Manual

Add the src/ directory to the Delphi Library Path. No component installation is required.

Required uses

uses
  SimpleInterface,    // Interfaces (iSimpleDAO, iSimpleQuery, etc)
  SimpleDAO,          // TSimpleDAO<T>
  SimpleAttributes,   // Attributes (Tabela, Campo, PK, etc)
  SimpleQueryFiredac; // Connection driver (or another driver)

Entity Mapping

Entities are Delphi classes mapped to database tables via RTTI attributes. Properties must be declared in the published section.

uses SimpleAttributes;

type
  [Tabela('PEDIDO')]
  TPEDIDO = class
  private
    FID: Integer;
    FCLIENTE: String;
    FDATAPEDIDO: TDatetime;
    FVALORTOTAL: Currency;
    procedure SetID(const Value: Integer);
    procedure SetCLIENTE(const Value: String);
    procedure SetDATAPEDIDO(const Value: TDatetime);
    procedure SetVALORTOTAL(const Value: Currency);
  public
    constructor Create;
    destructor Destroy; override;
  published
    [Campo('ID'), Pk, AutoInc]
    property ID: Integer read FID write SetID;
    [Campo('NOME')]
    property CLIENTE: String read FCLIENTE write SetCLIENTE;
    [Campo('DATA')]
    property DATAPEDIDO: TDatetime read FDATAPEDIDO write SetDATAPEDIDO;
    [Campo('VALOR')]
    property VALORTOTAL: Currency read FVALORTOTAL write SetVALORTOTAL;
  end;

When the property name matches the database column name, the [Campo] attribute is optional. Example: property ID automatically maps to column ID.

Entity Rules

All Attributes

All attributes are declared in SimpleAttributes.pas.

Mapping

AttributeTargetDescription
Tabela('NAME')ClassMaps class to database table
Campo('NAME')PropertyMaps property to database column

Keys and Auto-Increment

AttributeDescription
PKPrimary key (exactly one per entity)
FKForeign key
AutoIncAuto-increment (automatically excluded from INSERT)

Constraints

AttributeDescription
NotNullRequired field (non-empty string, non-zero date)
NotZeroNumeric value cannot be zero
IgnoreIgnores property in all SQL operations and serialization
NumberOnlyAccepts only numbers

Validation

AttributeDescription
EmailValidates email format
MinValue(n)Minimum numeric value
MaxValue(n)Maximum numeric value
Regex('pattern', 'msg')Validation via regular expression
Format(maxSize, precision)Maximum size, minimum size, precision or mask

Display and Binding

AttributeDescription
Display('Label')Display name for grids/forms
Bind('FIELD')Binds visual component to entity property

Relationships

AttributeCardinalityLoading
HasOne('Entity', 'FK')1:1Eager (automatic on Find)
BelongsTo('Entity', 'FK')N:1Eager (automatic on Find)
HasMany('Entity', 'FK')1:NLazy (TSimpleLazyLoader)
BelongsToMany('Entity', 'FK')M:NManual

Special

AttributeDescription
SoftDelete('FIELD')On the class — enables logical deletion (0=active, 1=deleted)
Enumerator('TYPE')PostgreSQL enum type cast

Initialization

var
  Conn: iSimpleQuery;
  DAOPedido: iSimpleDAO<TPEDIDO>;
begin
  // 1. Create connection via driver
  Conn := TSimpleQueryFiredac.New(FDConnection1);

  // 2. Create typed DAO
  DAOPedido := TSimpleDAO<TPEDIDO>.New(Conn);

  // 3. (Optional) Bind DataSource and Form
  DAOPedido
    .DataSource(DataSource1)  // data in DataSet for grids
    .BindForm(Self);          // automatic component binding
end;

iSimpleQuery abstracts the database driver. iSimpleDAO<T> is the generic DAO that performs CRUD on any mapped entity.

CRUD Operations

Insert

// With object
var LPedido: TPEDIDO;
begin
  LPedido := TPEDIDO.Create;
  try
    LPedido.CLIENTE := 'John Doe';
    LPedido.DATAPEDIDO := Now;
    LPedido.VALORTOTAL := 150.50;
    DAOPedido.Insert(LPedido);
  finally
    LPedido.Free;
  end;
end;

// With Bind (VCL/FMX — picks values from form automatically)
DAOPedido.Insert;

Update

// With object (PK identifies the record)
var LPedido: TPEDIDO;
begin
  LPedido := TPEDIDO.Create;
  try
    LPedido.ID := 1;
    LPedido.CLIENTE := 'Updated Name';
    LPedido.DATAPEDIDO := Now;
    LPedido.VALORTOTAL := 200.00;
    DAOPedido.Update(LPedido);
  finally
    LPedido.Free;
  end;
end;

// With Bind
DAOPedido.Update;

Delete

// With object (uses PK)
DAOPedido.Delete(LPedido);

// With Bind (uses PK from form)
DAOPedido.Delete;

// By field and value
DAOPedido.Delete('ID', '1');

// Force Delete (ignores SoftDelete)
DAOPedido.ForceDelete(LPedido);

Find (Select)

// All records (populates DataSource if bound)
DAOPedido.Find;

// Returns object list
var LList: TObjectList<TPEDIDO>;
begin
  LList := TObjectList<TPEDIDO>.Create;
  try
    DAOPedido.Find(LList);
    for I := 0 to LList.Count - 1 do
      Writeln(LList[I].CLIENTE);
  finally
    LList.Free;
  end;
end;

// Find by ID (returns object — caller must Free)
var LPedido: TPEDIDO;
begin
  LPedido := DAOPedido.Find(1);
  try
    if Assigned(LPedido) then
      Writeln(LPedido.CLIENTE);
  finally
    LPedido.Free;
  end;
end;

// Find by field/value
DAOPedido.Find('CLIENTE', 'John');

// Last ID and last record
DAOPedido.LastID;
DAOPedido.LastRecord;

Fluent SQL Builder

iSimpleDAOSQLAttribute<T> allows building custom queries via fluent interface:

DAOPedido
  .SQL
    .Fields('ID, NOME, VALOR')            // specific fields
    .Where('VALOR > 100')                 // WHERE clause
    .OrderBy('NOME')                       // ordering
    .GroupBy('CATEGORIA')                  // grouping
    .Join('INNER JOIN CLIENTE ON CLIENTE.ID = PEDIDO.ID_CLIENTE')
    .Skip(10)                              // skip N records
    .Take(20)                              // limit to N records
  .&End                                    // return to DAO
  .Find;                                   // execute

The .&End method (with & to escape the reserved word) returns to iSimpleDAO<T> to execute the built query.

Pagination

Pagination is supported via Skip(n) and Take(n) with SQL generated automatically based on the database:

// Fetch records 11 to 30
DAOPedido.SQL.Skip(10).Take(20).&End.Find(LList);
DatabaseGenerated SQLPosition
FirebirdSELECT FIRST 20 SKIP 10 ...After SELECT
MySQL... LIMIT 20 OFFSET 10End of query
SQLite... LIMIT 20 OFFSET 10End of query
Oracle... OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLYEnd of query

The database type is defined by the TSQLType of the query driver.

Validation

TSimpleValidator validates entities via RTTI attributes. Raises ESimpleValidator with accumulated error messages.

uses SimpleValidator;

var LCliente: TCliente;
begin
  LCliente := TCliente.Create;
  try
    LCliente.Parse(Self);              // fill via form Bind
    TSimpleValidator.Validate(LCliente); // validate — raises exception if invalid
    DAOCliente.Insert(LCliente);
  finally
    LCliente.Free;
  end;
end;

Validation attributes

AttributeWhat it validates
NotNullNon-empty string, non-zero date
NotZeroNumeric different from zero
EmailValid email format
MinValue(n)Numeric value >= n
MaxValue(n)Numeric value <= n
Regex('pattern', 'msg')Match against regular expression
Format(max, precision)String length and numeric precision

Relationships

HasOne / BelongsTo (Eager Loading)

Automatically loaded by the DAO on Find:

[Tabela('ITEM_PEDIDO')]
TItemPedido = class
published
  [Campo('ID'), PK, AutoInc]
  property ID: Integer ...;

  [Campo('ID_PEDIDO'), FK]
  property IdPedido: Integer ...;

  [HasOne('PEDIDO', 'ID_PEDIDO')]
  property Pedido: TPedido ...;
end;

HasMany (Lazy Loading)

Uses TSimpleLazyLoader<T> from SimpleProxy.pas. Data is loaded on first call to Count or ToArray:

uses SimpleProxy;

// In constructor or on demand:
FItens := TSimpleLazyLoader<TItemPedido>.Create(Query, 'ID_PEDIDO', Self.Id);

Soft Delete

Logical deletion — records are not removed from the database, only flagged.

[Tabela('PRODUTO'), SoftDelete('DELETADO')]
TProduto = class
published
  [Campo('ID'), PK, AutoInc]
  property ID: Integer ...;
  [Campo('DELETADO')]
  property Deletado: Integer ...;  // 0=active, 1=deleted
end;

Automatic behavior

OperationGenerated SQL
Delete(entity)UPDATE PRODUTO SET DELETADO = 1 WHERE ID = :ID
FindSELECT ... WHERE DELETADO = 0 (automatic filter)
ForceDelete(entity)DELETE FROM PRODUTO WHERE ID = :ID (ignores soft delete)

Batch Operations

Batch operations with automatic transaction (StartTransaction/Commit/Rollback):

var LList: TObjectList<TPEDIDO>;
begin
  LList := TObjectList<TPEDIDO>.Create;
  try
    // Fill list...
    DAOPedido.InsertBatch(LList);   // inserts all in transaction
    DAOPedido.UpdateBatch(LList);   // updates all in transaction
    DAOPedido.DeleteBatch(LList);   // deletes all in transaction
  finally
    LList.Free;
  end;
end;

If any operation fails, the transaction automatically rolls back and the exception is re-raised.

Transactions

Explicit transaction control via iSimpleQuery:

var LQuery: iSimpleQuery;
begin
  LQuery := TSimpleQueryFiredac.New(Connection);

  LQuery.StartTransaction;
  try
    // Operations...
    LQuery.Commit;
  except
    LQuery.Rollback;
    raise;
  end;
end;
MethodDescription
StartTransactionStarts transaction (checks if already active)
CommitCommits active transaction
RollbackRolls back active transaction
InTransactionReturns True if transaction is active
&EndTransactionAlias for Commit

Query Logging

Logs SQL, parameters and execution time:

uses SimpleLogger;

DAOPedido.Logger(TSimpleQueryLoggerConsole.New);

// From this point, every DAO operation will be logged:
// SQL: SELECT ID, NOME FROM PEDIDO WHERE ID = :ID
// Params: ID=1
// Duration: 3ms

TSimpleQueryLoggerConsole sends to OutputDebugString (Windows) or Writeln (console). Implement iSimpleQueryLogger for custom loggers.

Database Drivers

All drivers implement iSimpleQuery — swap the driver without changing any DAO code.

DriverClassConnectionUnit
FireDACTSimpleQueryFiredacTFDConnectionSimpleQueryFiredac.pas
UniDACTSimpleQueryUnidacTUniConnectionSimpleQueryUnidac.pas
ZeosTSimpleQueryZeosTZConnectionSimpleQueryZeos.pas
RestDWTSimpleQueryRestDWTRESTDWClientSQLSimpleQueryRestDW.pas
Horse (REST)TSimpleQueryHorseURL stringSimpleQueryHorse.pas

Example: switching drivers

// FireDAC (local database)
LDAO := TSimpleDAO<T>.New(TSimpleQueryFiredac.New(FDConnection1));

// UniDAC (local database)
LDAO := TSimpleDAO<T>.New(TSimpleQueryUnidac.New(UniConnection1));

// Horse REST (remote server)
LDAO := TSimpleDAO<T>.New(TSimpleQueryHorse.New('http://server:9000'));

// DAO code is IDENTICAL for all drivers

Horse Server NEW

Auto-generates 5 CRUD routes on Horse (ExpxHorse) per entity with a single line:

uses Horse, SimpleInterface, SimpleQueryFiredac, SimpleHorseRouter;

// One line = 5 REST routes
TSimpleHorseRouter.RegisterEntity<TPEDIDO>(THorse, LQuery);

THorse.Listen(9000);

Generated routes

MethodRouteStatusResponse
GET/pedido?skip=N&take=N200{"data": [...], "count": N}
GET/pedido/:id200/404Entity JSON or {"error": "Not found"}
POST/pedido201Created entity JSON
PUT/pedido/:id200Updated entity JSON
DELETE/pedido/:id204No body

The path is derived from the [Tabela] attribute lowercased, or customized via 3rd parameter.

Callbacks

TSimpleHorseRouter.RegisterEntity<TPEDIDO>(THorse, LQuery)
  .OnBeforeInsert(
    procedure(aEntity: TObject; var aContinue: Boolean)
    begin
      TSimpleValidator.Validate(aEntity);
      aContinue := True;  // False returns 400
    end
  )
  .OnAfterInsert(
    procedure(aEntity: TObject)
    begin
      Writeln('Inserted: ', TPEDIDO(aEntity).CLIENTE);
    end
  )
  .OnBeforeUpdate(
    procedure(aEntity: TObject; var aContinue: Boolean)
    begin
      aContinue := True;
    end
  )
  .OnBeforeDelete(
    procedure(aId: string; var aContinue: Boolean)
    begin
      aContinue := True;
    end
  );

Horse Client REST NEW

TSimpleQueryHorse implements iSimpleQuery via HTTP. The only difference from direct database access is the query creation line:

// BEFORE: direct database access
LDAO := TSimpleDAO<TPEDIDO>.New(TSimpleQueryFiredac.New(Connection));

// AFTER: via Horse server (same interface!)
LDAO := TSimpleDAO<TPEDIDO>.New(TSimpleQueryHorse.New('http://server:9000'));

// The rest of the code is IDENTICAL
LDAO.Insert(LPedido);
LDAO.Find(LList);
LDAO.SQL.Skip(0).Take(10).&End.Find(LList);

Authentication

// Bearer token
TSimpleQueryHorse.New('http://server:9000', 'my-token');

// Custom headers
var LQuery: iSimpleQuery;
begin
  LQuery := TSimpleQueryHorse.New('http://server:9000');
  TSimpleQueryHorse(LQuery).OnBeforeRequest(
    procedure(aHeaders: TStrings)
    begin
      aHeaders.Values['X-Custom-Header'] := 'value';
    end
  );
  LDAO := TSimpleDAO<TPEDIDO>.New(LQuery);
end;

Transactions are no-ops on the REST driver (HTTP is stateless). InTransaction always returns False.

SimpleSerializer NEW

Converts entities to JSON and back via RTTI, using [Campo] names as JSON keys:

uses SimpleSerializer;

// Entity -> JSON
var LJSON: TJSONObject;
LJSON := TSimpleSerializer.EntityToJSON<TPEDIDO>(LPedido);
// {"ID": 1, "NOME": "John", "DATA": "2026-03-08T10:00:00", "VALOR": 150.5}

// JSON -> Entity (caller must Free the result)
var LPedido: TPEDIDO;
LPedido := TSimpleSerializer.JSONToEntity<TPEDIDO>(LJSON);

// Lists
var LArray: TJSONArray;
LArray := TSimpleSerializer.EntityListToJSONArray<TPEDIDO>(LList);

var LList: TObjectList<TPEDIDO>;
LList := TSimpleSerializer.JSONArrayToEntityList<TPEDIDO>(LArray);

Supported types

DelphiJSON
Stringstring
Integernumber
Int64number
Double/Currencynumber
TDateTimeISO8601 string
Booleantrue/false

Form Binding

SimpleORM automatically binds visual components to entity properties:

type
  TForm1 = class(TForm)
    [Bind('CLIENTE')]
    Edit1: TEdit;
    [Bind('ID')]
    Edit2: TEdit;
    [Bind('VALORTOTAL')]
    Edit3: TEdit;
    [Bind('DATAPEDIDO')]
    DateTimePicker1: TDateTimePicker;
  end;

The [Bind] attribute on the component indicates which entity property (or column) it represents. With binding configured:

// Insert picks values from components automatically
DAOPedido.Insert;

// Update as well
DAOPedido.Update;

// Find populates components with database data
DAOPedido.Find;

Binding works with VCL (TEdit, TComboBox, TCheckBox, TDateTimePicker, etc) and FMX. Requires compilation WITHOUT {$DEFINE CONSOLE}.

Interface API

All interfaces are declared in SimpleInterface.pas.

iSimpleDAO<T>

MethodReturnDescription
Insert(aValue: T)iSimpleDAO<T>Inserts entity
Update(aValue: T)iSimpleDAO<T>Updates by PK
Delete(aValue: T)iSimpleDAO<T>Deletes by PK (or soft delete)
Delete(aField, aValue)iSimpleDAO<T>Deletes by field/value
ForceDelete(aValue: T)iSimpleDAO<T>Physically deletes (ignores soft delete)
FindiSimpleDAO<T>Finds all (populates DataSource)
Find(var aList)iSimpleDAO<T>Finds all into object list
Find(aId: Integer)TFinds by ID (returns object)
Find(aKey, aValue)iSimpleDAO<T>Finds by field/value
InsertBatch(aList)iSimpleDAO<T>Batch insert with transaction
UpdateBatch(aList)iSimpleDAO<T>Batch update with transaction
DeleteBatch(aList)iSimpleDAO<T>Batch delete with transaction
LastIDiSimpleDAO<T>Gets last inserted ID
LastRecordiSimpleDAO<T>Gets last record
SQLiSimpleDAOSQLAttribute<T>Accesses fluent SQL builder
Logger(aLogger)iSimpleDAO<T>Sets query logger
DataSource(aDS)iSimpleDAO<T>Binds DataSource
BindForm(aForm)iSimpleDAO<T>Binds form for binding

iSimpleDAOSQLAttribute<T>

MethodDescription
Fields(aSQL)SELECT fields
Where(aSQL)WHERE clause
OrderBy(aSQL)Ordering
GroupBy(aSQL)Grouping
Join(aSQL)JOINs
Skip(n)Skip N records (pagination)
Take(n)Limit to N records (pagination)
ClearClears all filters
&EndReturns to iSimpleDAO<T>

iSimpleQuery

MethodReturnDescription
SQLTStringsSQL text
ParamsTParamsQuery parameters
ExecSQLiSimpleQueryExecutes INSERT/UPDATE/DELETE
DataSetTDataSetSELECT result
Open(aSQL)iSimpleQuerySets SQL and opens
OpeniSimpleQueryOpens with already set SQL
StartTransactioniSimpleQueryStarts transaction
CommitiSimpleQueryCommits transaction
RollbackiSimpleQueryRolls back transaction
&EndTransactioniSimpleQueryAlias for Commit
InTransactionBooleanTrue if transaction is active
SQLTypeTSQLTypeDatabase type

Database Types (TSQLType)

TSQLType = (Firebird, MySQL, SQLite, Oracle);

Defined in SimpleTypes.pas. Affects SQL generation for pagination. The type is passed in the driver constructor:

// Firebird (default)
TSimpleQueryFiredac.New(Conn);

// MySQL
TSimpleQueryFiredac.New(Conn, TSQLType.MySQL);

// SQLite
TSimpleQueryFiredac.New(Conn, TSQLType.SQLite);

// Oracle
TSimpleQueryFiredac.New(Conn, TSQLType.Oracle);

Unit Tests (DUnit) NEW

SimpleORM has a complete unit test suite using DUnit, covering attributes, RTTI helpers, SQL generation, validation and serialization.

Structure

tests/
  SimpleORMTests.dpr          -- Console test runner
  Entities/
    TestEntities.pas           -- Test entities decorated with attributes
  TestSimpleAttributes.pas     -- Attribute tests (22 tests)
  TestSimpleRTTIHelper.pas     -- RTTI helper tests (26 tests)
  TestSimpleSQL.pas            -- SQL generation tests (20 tests)
  TestSimpleValidator.pas      -- Validation tests (21 tests)
  TestSimpleSerializer.pas     -- JSON serialization tests (10 tests)

Running Tests

// 1. Open tests/SimpleORMTests.dpr in Delphi IDE
// 2. Compile (IDE generates .dproj and .res automatically)
// 3. Run

// With pause after execution:
SimpleORMTests.exe -pause

Test Coverage

UnitTestsCoverage
SimpleAttributes22Constructors, properties, GetNumericMask, relationships
SimpleRTTIHelper26IsNotNull, IsAutoInc, FieldName, DisplayName, EhChavePrimaria, SoftDelete, ValueHelper
SimpleSQL20INSERT, UPDATE, DELETE, SELECT, pagination (Firebird/MySQL/SQLite/Oracle), SoftDelete
SimpleValidator21NotNull, NotZero, Format, Email, MinValue, MaxValue, Regex, exception
SimpleSerializer10EntityToJSON, JSONToEntity, lists, roundtrip, ignored fields

Adding New Tests

unit TestSimpleNewFeature;

interface

uses
  TestFramework;

type
  TTestNewFeature = class(TTestCase)
  published
    procedure TestScenario_ShouldPass;
    procedure TestScenario_ShouldFail;
  end;

implementation

procedure TTestNewFeature.TestScenario_ShouldPass;
begin
  CheckTrue(True, 'Test description');
end;

procedure TTestNewFeature.TestScenario_ShouldFail;
begin
  // test error scenario
end;

initialization
  RegisterTest('NewFeature', TTestNewFeature.Suite);

end.
Rule: Every new feature implemented in SimpleORM MUST include DUnit unit tests. Add the test unit to the uses clause of SimpleORMTests.dpr.

AI Entity Enrichment NEW

SimpleORM allows automatic entity enrichment with AI. Simply decorate properties with AI attributes and processing happens automatically during Insert/Update.

AI Attributes

AttributeParametersDescription
AIGeneratedpromptTemplate: StringGenerates content via LLM. Template supports {PropertyName} for interpolation
AISummarizesourceProperty: String, maxLength: IntegerSummarizes content from another property
AITranslatesourceProperty: String, targetLanguage: StringTranslates content from another property
AIClassifysourceProperty: String, categories: StringClassifies content into comma-separated categories
AIValidaterule: String, errorMessage: StringValidates content against a rule. Raises exception if invalid

Entity example with AI attributes

[Tabela('PRODUTOS')]
TProduto = class
published
  [Campo('NOME')]
  property NOME: String read FNOME write SetNOME;

  [Campo('DESCRICAO')]
  property DESCRICAO: String read FDESCRICAO write SetDESCRICAO;

  [Campo('RESUMO'), AISummarize('DESCRICAO', 100)]
  property RESUMO: String read FRESUMO write SetRESUMO;

  [Campo('CATEGORIA'), AIClassify('DESCRICAO', 'Electronics,Clothing,Food')]
  property CATEGORIA: String read FCATEGORIA write SetCATEGORIA;

  [Campo('DESCRICAO_EN'), AITranslate('DESCRICAO', 'English')]
  property DESCRICAO_EN: String read FDESCRICAO_EN write SetDESCRICAO_EN;

  [Campo('SLOGAN'), AIGenerated('Create a slogan for {NOME}')]
  property SLOGAN: String read FSLOGAN write SetSLOGAN;
end;

Usage with TSimpleDAO (automatic)

// Configure AI client on DAO
DAOProduto := TSimpleDAO<TProduto>
  .New(Conn)
  .AIClient(TSimpleAIClient.New('claude', 'your-api-key'));

// AI processes AUTOMATICALLY before Insert/Update
DAOProduto.Insert(Produto);
// Properties with AI attributes are filled by the LLM!

Direct Processor usage

LAIClient := TSimpleAIClient.New('claude', 'your-api-key');
LProcessor := TSimpleAIProcessor.New(LAIClient);
try
  LProcessor.Process(Produto);
  // AI properties have been filled
finally
  FreeAndNil(LProcessor);
end;

Supported providers

ProviderDefault modelUsage
claudeclaude-sonnet-4-20250514TSimpleAIClient.New('claude', 'api-key')
openaigpt-4o-miniTSimpleAIClient.New('openai', 'api-key')

The AI client is optional. If not configured via AIClient(), the DAO works normally without AI processing.

AI Query NEW

SimpleORM allows querying the database in natural language, automatically translating to SQL via LLM.

Methods

MethodInputOutputDescription
NaturalLanguageQueryquestion: StringTDataSetTranslates to SQL, executes, returns result
AskQuestionquestion: StringStringTranslates, executes, LLM responds in natural language
ExplainQuerysql: StringStringExplains SQL in natural language
SuggestQueryobjective: StringStringSuggests SQL based on objective

Usage example

AIQuery := TSimpleAIQuery.New(Query, AIClient);
AIQuery
  .RegisterEntity<TCliente>
  .RegisterEntity<TPedido>
  .RegisterEntity<TProduto>;

// Natural language question - returns TDataSet
LDataSet := AIQuery.NaturalLanguageQuery('Top 5 customers by purchase value in 2025');

// Natural text response
LResponse := AIQuery.AskQuestion('What is the average ticket?');

// Explain existing SQL
LExplanation := AIQuery.ExplainQuery('SELECT AVG(VALOR) FROM PEDIDOS');

// Suggest SQL for an objective
LSQL := AIQuery.SuggestQuery('Inactive customers for more than 90 days');

Security

For security, only SELECT SQL is allowed. Keywords like INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, TRUNCATE, EXEC, GRANT and REVOKE are automatically blocked. Semicolons are also blocked to prevent multi-statement injection.

Result limiting

// Limit to 50 results (default: 100)
AIQuery.MaxRows(50);

// Pagination is applied automatically based on database type:
// Firebird: SELECT FIRST N ...
// MySQL/SQLite: ... LIMIT N
// Oracle: ... FETCH NEXT N ROWS ONLY

MCP Server (AI Integration) NEW

SimpleORM includes a built-in MCP (Model Context Protocol) server that allows AI assistants like Claude and Cursor to interact directly with your application's database, with granular permissions and security.

uses SimpleMCPServer, SimpleMCPTypes;

Server := TSimpleMCPServer.New;
Server
  .RegisterEntity<TUsuario>(Query, [mcpRead, mcpInsert, mcpUpdate])
  .RegisterEntity<TProduto>(Query, [mcpRead, mcpCount])
  .EnableRawQuery(Query)
  .StartStdio;  // or .Token('secret').StartHttp(9000)

Features

Complete MCP Server documentation →

Rules NEW

Declarative rules on the entity that execute automatically during CRUD. Can be deterministic (Delphi expressions) or intelligent (via AI).

Attributes

AttributeParametersDescription
Ruleexpression, action, messageDeterministic rule evaluated against entity properties
AIRuledescription, actionIntelligent rule evaluated by the LLM

Example

[Tabela('PEDIDOS')]
[Rule('VALOR > 0', raBeforeInsert, 'Value must be positive')]
[Rule('STATUS <> ''CANCELADO''', raBeforeUpdate, 'Cancelled order cannot be modified')]
[AIRule('Verify if the address is valid', raBeforeInsert)]
TPedido = class
published
  [Campo('VALOR')]
  property VALOR: Currency;
  [Campo('STATUS')]
  property STATUS: String;
end;

Rules are automatically detected via RTTI. No configuration on the DAO is required.

Skills NEW

Reusable plugins that connect to the DAO via fluent API. Execute before or after CRUD operations. SimpleORM offers 19 ready-made Skills organized in 3 categories.

Full Skills Documentation →

Built-in Skills (SimpleSkill.pas)

8 infrastructure Skills for logging, auditing, validation and integration.

SkillDescription
TSkillLogLogs operation via Logger or OutputDebugString/Writeln
TSkillNotifyFires TProc<TObject> callback
TSkillAuditInserts record into audit table
TSkillTimestampAuto-fills date fields
TSkillValidateCalls TSimpleValidator.Validate automatically
TSkillGuardDeleteBlocks delete with dependent records
TSkillHistorySaves snapshot before update/delete
TSkillWebhookFire-and-forget HTTP POST with JSON

ERP Skills (SimpleSkill.pas)

4 Skills for Brazilian ERPs: sequential numbering, total calculation, stock and installments. Includes [CPF] and [CNPJ] attributes for document validation.

SkillDescription
TSkillSequenceSequential numbering via control table
TSkillCalcTotalCalculates total: Qty * Price - Discount
TSkillStockMoveRecords stock movement
TSkillDuplicateGenerates financial installments

AI Skills (SimpleAISkill.pas)

7 AI-powered Skills for data enrichment, validation/moderation and sentiment analysis via LLM.

SkillDescription
TSkillAIEnrichGenerates content via prompt template with {PropertyName}
TSkillAITranslateAutomatic translation between fields
TSkillAISummarizeAutomatic text summarization
TSkillAITagsAutomatic tag/keyword generation
TSkillAIModerateContent moderation with blocking
TSkillAIValidateNatural language data validation
TSkillAISentimentSentiment analysis (POSITIVO/NEGATIVO/NEUTRO)

Example

DAO := TSimpleDAO<TPedido>.New(Query)
  .Skill(TSkillTimestamp.New('CREATED_AT', srBeforeInsert))
  .Skill(TSkillValidate.New(srBeforeInsert))
  .Skill(TSkillLog.New('App', srAfterInsert))
  .Skill(TSkillCalcTotal.New('VALOR_TOTAL', 'QUANTIDADE', 'PRECO_UNITARIO', 'DESCONTO'))
  .Skill(TSkillAISentiment.New('COMENTARIO', 'SENTIMENTO'));

DAO.Insert(Pedido);

Skills are optional. If no skill is registered, the DAO works normally. For detailed documentation with complete examples, parameters, DDL and use cases, see the dedicated Skills page.

Agents NEW

Orchestrators that combine Rules, Skills and CRUD operations. Reactive mode (responds to events) and proactive mode (natural language via AI).

Reactive Mode

LAgent := TSimpleAgent.New;
LAgent.When(TPedido, aoAfterInsert)
  .Condition(function(aEntity: TObject): Boolean
    begin
      Result := TPedido(aEntity).VALOR > 5000;
    end)
  .Execute(TSkillSendEmail.New('manager@company.com'))
  .Execute(TSkillLog.New('high-value'));

DAOPedido := TSimpleDAO<TPedido>.New(Conn).Agent(LAgent);
DAOPedido.Insert(Pedido); // Agent reacts automatically

Proactive Mode

LAgent := TSimpleAgent.New(Conn, AIClient);
LAgent.RegisterEntity<TPedido>.RegisterEntity<TCliente>;

// Plan before executing (SafeMode)
LPlan := LAgent.Plan('List pending orders from today');
Writeln(LPlan.Description);
Writeln(LPlan.Risk); // LOW, MEDIUM or HIGH
LPlan.Execute;

Proactive mode requires SafeMode (default: True). Use Plan() to inspect before Execute().

Supabase Integration NEW

SimpleORM supports direct connection to Supabase via PostgREST API, with no intermediate server required. The integration includes 3 components:

ComponentUnitDescription
TSimpleQuerySupabaseSimpleQuerySupabaseCRUD driver implementing iSimpleQuery — INSERT, UPDATE, DELETE, SELECT via REST
TSimpleSupabaseAuthSimpleSupabaseAuthAuthentication — SignIn, SignUp, SignOut, JWT auto-refresh, Row Level Security
TSimpleSupabaseRealtimeSimpleSupabaseRealtimeRealtime monitoring — global and per-table callbacks via polling

Quick Example

uses SimpleQuerySupabase, SimpleSupabaseAuth;

// Basic connection
var LQuery := TSimpleQuerySupabase.New(
  'https://your-project.supabase.co',
  'your-api-key'
);

// CRUD identical to any other driver
var LDAO := TSimpleDAO<TProduct>.New(LQuery);
LDAO.Insert(LProduct);
LDAO.Find;
LDAO.Update(LProduct);
LDAO.Delete(LProduct);

// With authentication and Row Level Security
var LAuth := TSimpleSupabaseAuth.New(URL, AnonKey);
LAuth.SignIn('user@email.com', 'password123');
var LQueryAuth := TSimpleQuerySupabase.New(URL, AnonKey, LAuth);

Note: The driver uses the native System.Net.HttpClient, with no external dependencies. Transactions are no-ops (REST is stateless).

Full Supabase Documentation →
Complete API reference, detailed examples for CRUD, Auth, Realtime, Row Level Security, troubleshooting, and more.

GitHub Issue Skill NEW

SimpleORM can automatically create GitHub Issues when CRUD operations fail or as auditing for successful operations.

ComponentDescription
TSkillGitHubIssueSkill that creates Issues via GitHub REST API — fire-and-forget
TSkillRunModeEnum srmNormal (audit) or srmOnError (failures)
OnErrorGeneric DAO callback for error handling

Quick Example

uses SimpleSkill, SimpleTypes;

// OnError mode — creates Issue when Insert fails
LDAO := TSimpleDAO<TProduct>.New(LQuery)
  .Skill(TSkillGitHubIssue.New(
    'myuser/myrepo',
    'ghp_token123',
    srAfterInsert,
    srmOnError
  ))
  .OnError(procedure(aEntity: TObject; E: Exception)
    begin
      Writeln('Error: ', E.Message);
    end);

// Normal mode with template — audit Deletes
LDAO := TSimpleDAO<TProduct>.New(LQuery)
  .Skill(TSkillGitHubIssue.New(
    'myuser/myrepo',
    'ghp_token123',
    srAfterDelete,
    srmNormal
  ).Labels(['audit', 'delete'])
   .TitleTemplate('[Audit] {entity} deleted'));

Template Placeholders

PlaceholderDescription
{entity}Entity table name
{operation}INSERT, UPDATE or DELETE
{error}Error message (empty in Normal mode)
{timestamp}ISO8601 date/time

Note: Fire-and-forget — HTTP errors are logged but never interrupt the CRUD flow. Requires a GitHub Personal Access Token with repo permission.

Data Migration NEW

SimpleORM provides a fluent framework for data migration between databases, CSV files and JSON. It allows field mapping, transformations, default values and detailed reporting.

ComponentDescription
TSimpleDataMigrationMain class with fluent API to configure and execute migrations
TFieldMapField mapping with Field, Transform, DefaultValue, Lookup and Ignore
TFieldTransformBuilt-in transformations for data manipulation during migration
TMigrationReportReport with TotalRecords, Migrated, Failed, Skipped, ToJSON and ToCSV
TTableReportPer-table report with detailed error list
TCSVReader / TCSVWriterCSV reader and writer with header support

DB-to-DB Migration

uses SimpleDataMigration;

LMigration := TSimpleDataMigration.New
  .Source(LQuerySource)
  .Target(LQueryTarget)
  .Map('OLD_CUSTOMERS', 'CUSTOMERS')
    .Field('FULL_NAME', 'NAME')
    .Transform('FULL_NAME', 'NAME', TFieldTransform.Upper)
    .DefaultValue('STATUS', '1')
    .Field('TAX_ID', 'DOCUMENT')
    .Transform('TAX_ID', 'DOCUMENT', TFieldTransform.Trim)
    .Ignore('OBSOLETE_FIELD')
  .&End
  .OnProgress(procedure(aTable: String; aCurrent, aTotal: Integer)
    begin
      Writeln(SysUtils.Format('%s: %d/%d', [aTable, aCurrent, aTotal]));
    end)
  .OnError(procedure(aError: TMigrationError; var aSkip: Boolean)
    begin
      Writeln(SysUtils.Format('Error in %s record %d: %s',
        [aError.SourceTable, aError.RecordIndex, aError.ErrorMessage]));
      aSkip := True;
    end);

LReport := LMigration.Execute;

CSV-to-DB Migration

LMigration := TSimpleDataMigration.New
  .Source('customers_export.csv', mfCSV)
  .Target(LQueryTarget)
  .Map('', 'CUSTOMERS')
    .Field('name', 'NAME')
    .Transform('name', 'NAME', TFieldTransform.Upper)
    .Field('email', 'EMAIL')
    .Transform('email', 'EMAIL', TFieldTransform.Lower)
    .DefaultValue('IMPORT_DATE', '2026-03-10')
  .&End;

LReport := LMigration.Execute;

Built-in Transforms

TransformDescriptionExample
TFieldTransform.UpperConverts to uppercase'john''JOHN'
TFieldTransform.LowerConverts to lowercase'JOHN''john'
TFieldTransform.TrimRemoves leading and trailing spaces' john ''john'
TFieldTransform.Replace('a', 'b')Replaces substring'abc''bbc'
TFieldTransform.DateFormat('dd/mm/yyyy', 'yyyy-mm-dd')Converts date format'10/03/2026''2026-03-10'
TFieldTransform.Split(';', 0)Splits string and returns part N'a;b;c''a'
TFieldTransform.Concat(' - ')Concatenates fields with separator'field1' + ' - ' + 'field2'
TFieldTransform.Custom(func)Custom transformation functionAny custom logic

Mapping Persistence

// Save mapping for reuse
LMigration.SaveToJSON('customer_migration.json');

// Load saved mapping
LMigration := TSimpleDataMigration.New
  .LoadFromJSON('customer_migration.json')
  .Source(LQuerySource)
  .Target(LQueryTarget);

LReport := LMigration.Execute;

Migration Report

LReport := LMigration.Execute;
try
  Writeln('Total: ', LReport.TotalRecords);
  Writeln('Migrated: ', LReport.Migrated);
  Writeln('Failed: ', LReport.Failed);
  Writeln('Skipped: ', LReport.Skipped);
  Writeln('Duration: ', LReport.DurationMs, 'ms');

  // Export report
  LJSON := LReport.ToJSON;  // returns TJSONObject (caller must free)
  try
    Writeln(LJSON.ToString);
  finally
    LJSON.Free;
  end;

  LCSVContent := LReport.ToCSV;  // returns String

  // Per-table details
  for LTable in LReport.Tables do
  begin
    Writeln(LTable.SourceTable, ' -> ', LTable.TargetTable, ': ',
      LTable.Migrated, ' migrated, ', LTable.Failed, ' failed');
    for LError in LTable.Errors do
      Writeln('  Record ', LError.RecordIndex, ' [', LError.FieldName, ']: ',
        LError.ErrorMessage);
  end;
finally
  LReport.Free;
end;

Note: Migration uses automatic transactions per table. On error, rollback is done only for the current table, preserving tables already migrated successfully.