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
[Tabela('NAME')]on the class — required[Campo('COLUMN')]on properties — required for SQL mapping- Exactly one
[PK]per entity - Properties in the
publishedsection — RTTI does not seepublic - Explicit getter/setter for each property
constructor Createanddestructor Destroy; overridealways present
All Attributes
All attributes are declared in SimpleAttributes.pas.
Mapping
| Attribute | Target | Description |
|---|---|---|
Tabela('NAME') | Class | Maps class to database table |
Campo('NAME') | Property | Maps property to database column |
Keys and Auto-Increment
| Attribute | Description |
|---|---|
PK | Primary key (exactly one per entity) |
FK | Foreign key |
AutoInc | Auto-increment (automatically excluded from INSERT) |
Constraints
| Attribute | Description |
|---|---|
NotNull | Required field (non-empty string, non-zero date) |
NotZero | Numeric value cannot be zero |
Ignore | Ignores property in all SQL operations and serialization |
NumberOnly | Accepts only numbers |
Validation
| Attribute | Description |
|---|---|
Email | Validates 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
| Attribute | Description |
|---|---|
Display('Label') | Display name for grids/forms |
Bind('FIELD') | Binds visual component to entity property |
Relationships
| Attribute | Cardinality | Loading |
|---|---|---|
HasOne('Entity', 'FK') | 1:1 | Eager (automatic on Find) |
BelongsTo('Entity', 'FK') | N:1 | Eager (automatic on Find) |
HasMany('Entity', 'FK') | 1:N | Lazy (TSimpleLazyLoader) |
BelongsToMany('Entity', 'FK') | M:N | Manual |
Special
| Attribute | Description |
|---|---|
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);
| Database | Generated SQL | Position |
|---|---|---|
| Firebird | SELECT FIRST 20 SKIP 10 ... | After SELECT |
| MySQL | ... LIMIT 20 OFFSET 10 | End of query |
| SQLite | ... LIMIT 20 OFFSET 10 | End of query |
| Oracle | ... OFFSET 10 ROWS FETCH NEXT 20 ROWS ONLY | End 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
| Attribute | What it validates |
|---|---|
NotNull | Non-empty string, non-zero date |
NotZero | Numeric different from zero |
Email | Valid 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
| Operation | Generated SQL |
|---|---|
Delete(entity) | UPDATE PRODUTO SET DELETADO = 1 WHERE ID = :ID |
Find | SELECT ... 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;
| Method | Description |
|---|---|
StartTransaction | Starts transaction (checks if already active) |
Commit | Commits active transaction |
Rollback | Rolls back active transaction |
InTransaction | Returns True if transaction is active |
&EndTransaction | Alias 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.
| Driver | Class | Connection | Unit |
|---|---|---|---|
| FireDAC | TSimpleQueryFiredac | TFDConnection | SimpleQueryFiredac.pas |
| UniDAC | TSimpleQueryUnidac | TUniConnection | SimpleQueryUnidac.pas |
| Zeos | TSimpleQueryZeos | TZConnection | SimpleQueryZeos.pas |
| RestDW | TSimpleQueryRestDW | TRESTDWClientSQL | SimpleQueryRestDW.pas |
| Horse (REST) | TSimpleQueryHorse | URL string | SimpleQueryHorse.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
| Method | Route | Status | Response |
|---|---|---|---|
| GET | /pedido?skip=N&take=N | 200 | {"data": [...], "count": N} |
| GET | /pedido/:id | 200/404 | Entity JSON or {"error": "Not found"} |
| POST | /pedido | 201 | Created entity JSON |
| PUT | /pedido/:id | 200 | Updated entity JSON |
| DELETE | /pedido/:id | 204 | No 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
| Delphi | JSON |
|---|---|
String | string |
Integer | number |
Int64 | number |
Double/Currency | number |
TDateTime | ISO8601 string |
Boolean | true/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>
| Method | Return | Description |
|---|---|---|
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) |
Find | iSimpleDAO<T> | Finds all (populates DataSource) |
Find(var aList) | iSimpleDAO<T> | Finds all into object list |
Find(aId: Integer) | T | Finds 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 |
LastID | iSimpleDAO<T> | Gets last inserted ID |
LastRecord | iSimpleDAO<T> | Gets last record |
SQL | iSimpleDAOSQLAttribute<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>
| Method | Description |
|---|---|
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) |
Clear | Clears all filters |
&End | Returns to iSimpleDAO<T> |
iSimpleQuery
| Method | Return | Description |
|---|---|---|
SQL | TStrings | SQL text |
Params | TParams | Query parameters |
ExecSQL | iSimpleQuery | Executes INSERT/UPDATE/DELETE |
DataSet | TDataSet | SELECT result |
Open(aSQL) | iSimpleQuery | Sets SQL and opens |
Open | iSimpleQuery | Opens with already set SQL |
StartTransaction | iSimpleQuery | Starts transaction |
Commit | iSimpleQuery | Commits transaction |
Rollback | iSimpleQuery | Rolls back transaction |
&EndTransaction | iSimpleQuery | Alias for Commit |
InTransaction | Boolean | True if transaction is active |
SQLType | TSQLType | Database 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
| Unit | Tests | Coverage |
|---|---|---|
SimpleAttributes | 22 | Constructors, properties, GetNumericMask, relationships |
SimpleRTTIHelper | 26 | IsNotNull, IsAutoInc, FieldName, DisplayName, EhChavePrimaria, SoftDelete, ValueHelper |
SimpleSQL | 20 | INSERT, UPDATE, DELETE, SELECT, pagination (Firebird/MySQL/SQLite/Oracle), SoftDelete |
SimpleValidator | 21 | NotNull, NotZero, Format, Email, MinValue, MaxValue, Regex, exception |
SimpleSerializer | 10 | EntityToJSON, 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.
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
| Attribute | Parameters | Description |
|---|---|---|
| AIGenerated | promptTemplate: String | Generates content via LLM. Template supports {PropertyName} for interpolation |
| AISummarize | sourceProperty: String, maxLength: Integer | Summarizes content from another property |
| AITranslate | sourceProperty: String, targetLanguage: String | Translates content from another property |
| AIClassify | sourceProperty: String, categories: String | Classifies content into comma-separated categories |
| AIValidate | rule: String, errorMessage: String | Validates 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
| Provider | Default model | Usage |
|---|---|---|
| claude | claude-sonnet-4-20250514 | TSimpleAIClient.New('claude', 'api-key') |
| openai | gpt-4o-mini | TSimpleAIClient.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
| Method | Input | Output | Description |
|---|---|---|---|
| NaturalLanguageQuery | question: String | TDataSet | Translates to SQL, executes, returns result |
| AskQuestion | question: String | String | Translates, executes, LLM responds in natural language |
| ExplainQuery | sql: String | String | Explains SQL in natural language |
| SuggestQuery | objective: String | String | Suggests 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
- Automatically generates MCP tools from registered entities (query, get, insert, update, delete, count, describe, ddl)
- Granular permissions:
mcpRead,mcpInsert,mcpUpdate,mcpDelete,mcpCount,mcpDDL - Two transports: stdio (local) and HTTP via Horse (remote with Bearer token)
- Read-only raw SQL with injection protection
- Validation via SimpleValidator on insert/update
Rules NEW
Declarative rules on the entity that execute automatically during CRUD. Can be deterministic (Delphi expressions) or intelligent (via AI).
Attributes
| Attribute | Parameters | Description |
|---|---|---|
| Rule | expression, action, message | Deterministic rule evaluated against entity properties |
| AIRule | description, action | Intelligent 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.
Built-in Skills (SimpleSkill.pas)
8 infrastructure Skills for logging, auditing, validation and integration.
| Skill | Description |
|---|---|
TSkillLog | Logs operation via Logger or OutputDebugString/Writeln |
TSkillNotify | Fires TProc<TObject> callback |
TSkillAudit | Inserts record into audit table |
TSkillTimestamp | Auto-fills date fields |
TSkillValidate | Calls TSimpleValidator.Validate automatically |
TSkillGuardDelete | Blocks delete with dependent records |
TSkillHistory | Saves snapshot before update/delete |
TSkillWebhook | Fire-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.
| Skill | Description |
|---|---|
TSkillSequence | Sequential numbering via control table |
TSkillCalcTotal | Calculates total: Qty * Price - Discount |
TSkillStockMove | Records stock movement |
TSkillDuplicate | Generates financial installments |
AI Skills (SimpleAISkill.pas)
7 AI-powered Skills for data enrichment, validation/moderation and sentiment analysis via LLM.
| Skill | Description |
|---|---|
TSkillAIEnrich | Generates content via prompt template with {PropertyName} |
TSkillAITranslate | Automatic translation between fields |
TSkillAISummarize | Automatic text summarization |
TSkillAITags | Automatic tag/keyword generation |
TSkillAIModerate | Content moderation with blocking |
TSkillAIValidate | Natural language data validation |
TSkillAISentiment | Sentiment 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:
| Component | Unit | Description |
|---|---|---|
| TSimpleQuerySupabase | SimpleQuerySupabase | CRUD driver implementing iSimpleQuery — INSERT, UPDATE, DELETE, SELECT via REST |
| TSimpleSupabaseAuth | SimpleSupabaseAuth | Authentication — SignIn, SignUp, SignOut, JWT auto-refresh, Row Level Security |
| TSimpleSupabaseRealtime | SimpleSupabaseRealtime | Realtime 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.
| Component | Description |
|---|---|
| TSkillGitHubIssue | Skill that creates Issues via GitHub REST API — fire-and-forget |
| TSkillRunMode | Enum srmNormal (audit) or srmOnError (failures) |
| OnError | Generic 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
| Placeholder | Description |
|---|---|
{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.
| Component | Description |
|---|---|
| TSimpleDataMigration | Main class with fluent API to configure and execute migrations |
| TFieldMap | Field mapping with Field, Transform, DefaultValue, Lookup and Ignore |
| TFieldTransform | Built-in transformations for data manipulation during migration |
| TMigrationReport | Report with TotalRecords, Migrated, Failed, Skipped, ToJSON and ToCSV |
| TTableReport | Per-table report with detailed error list |
| TCSVReader / TCSVWriter | CSV 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
| Transform | Description | Example |
|---|---|---|
TFieldTransform.Upper | Converts to uppercase | 'john' → 'JOHN' |
TFieldTransform.Lower | Converts to lowercase | 'JOHN' → 'john' |
TFieldTransform.Trim | Removes 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 function | Any 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.