Skip to content

Commit 48b37b1

Browse files
author
David Lastrucci
committed
Change Tracking (Created, Updated & Deleted - At & By)
1 parent 08e7a62 commit 48b37b1

16 files changed

Lines changed: 602 additions & 21 deletions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
Notable changes to Trysil, in reverse chronological order.
44

5+
## Change Tracking & Soft Delete
6+
7+
- **Change tracking attributes**: `[TCreatedAt]`, `[TCreatedBy]`, `[TUpdatedAt]`, `[TUpdatedBy]`, `[TDeletedAt]`, `[TDeletedBy]` — automatic timestamps and user tracking on insert, update, and delete
8+
- **Soft delete**: entities with `[TDeletedAt]` use UPDATE instead of DELETE; all SELECT queries automatically exclude soft-deleted records (`DeletedAt IS NULL`)
9+
- **`IncludeDeleted`**: option on `TTFilter` and `TTFilterBuilder<T>` to include soft-deleted records in queries
10+
- **`OnGetCurrentUser`**: callback property on `TTContext` to provide the current user name for `*By` fields
11+
- **`TTChangeTrackingMap`**: mapping infrastructure for change tracking columns
12+
- **`TTSoftDeleteSyntax`**: SQL syntax class for soft delete UPDATE statements
13+
514
## Recent
615

716
- **Docs**: MkDocs Material documentation site, cookbook, demo READMEs

Docs/api-reference/attributes.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ Unit: `Trysil.Attributes`
1717
| `TRelation(table, fk, cascade)` | Class | Declares child relationship |
1818
| `TWhereClause(sql)` | Class | Adds fixed WHERE clause to all queries |
1919
| `TWhereClauseParameter(name, value)` | Class | Parameter for `TWhereClause` |
20+
| `TCreatedAt` | Field | Timestamp set on insert (`TTNullable<TDateTime>`) |
21+
| `TCreatedBy` | Field | User name set on insert (`String`) |
22+
| `TUpdatedAt` | Field | Timestamp set on update (`TTNullable<TDateTime>`) |
23+
| `TUpdatedBy` | Field | User name set on update (`String`) |
24+
| `TDeletedAt` | Field | Timestamp set on delete — enables soft delete (`TTNullable<TDateTime>`) |
25+
| `TDeletedBy` | Field | User name set on delete (`String`) |
2026

2127
### TTable
2228

@@ -108,6 +114,48 @@ Adds a fixed WHERE clause to every query on this entity. Parameters are **compil
108114

109115
`TWhereClauseParameter` constructors accept: `String`, `Integer`, `Int64`, `Double`, `Boolean`, `TDateTime`.
110116

117+
### Change Tracking Attributes
118+
119+
```pascal
120+
[TCreatedAt]
121+
[TColumn('CreatedAt')]
122+
FCreatedAt: TTNullable<TDateTime>;
123+
124+
[TCreatedBy]
125+
[TColumn('CreatedBy')]
126+
FCreatedBy: String;
127+
128+
[TUpdatedAt]
129+
[TColumn('UpdatedAt')]
130+
FUpdatedAt: TTNullable<TDateTime>;
131+
132+
[TUpdatedBy]
133+
[TColumn('UpdatedBy')]
134+
FUpdatedBy: String;
135+
136+
[TDeletedAt]
137+
[TColumn('DeletedAt')]
138+
FDeletedAt: TTNullable<TDateTime>;
139+
140+
[TDeletedBy]
141+
[TColumn('DeletedBy')]
142+
FDeletedBy: String;
143+
```
144+
145+
The resolver automatically populates these fields:
146+
147+
- **`TCreatedAt` / `TCreatedBy`** — set during `Insert` with `Now` and the value from `TTContext.OnGetCurrentUser`.
148+
- **`TUpdatedAt` / `TUpdatedBy`** — set during `Update`.
149+
- **`TDeletedAt` / `TDeletedBy`** — set during `Delete`. When `TDeletedAt` is present, delete becomes a **soft delete** (UPDATE instead of DELETE). All SELECT queries automatically add `DeletedAt IS NULL` to exclude soft-deleted records.
150+
151+
Type constraints:
152+
153+
- `*At` fields must be `TTNullable<TDateTime>` — validated at mapping time.
154+
- `*By` fields must be `String` — validated at mapping time.
155+
- Duplicate attributes of the same kind on the same entity raise `ETException`.
156+
157+
See [Entity Mapping — Change Tracking](../guide/entities.md#change-tracking) for a full example.
158+
111159
---
112160

113161
## Validation

Docs/guide/context.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ LContext.UpdateAll<TPerson>(LPersonList);
141141
LContext.Delete<TPerson>(LPerson);
142142
```
143143

144+
If the entity has a `[TDeletedAt]` column, `Delete` performs a **soft delete** (UPDATE) instead of a SQL DELETE. See [Entity Mapping — Soft Delete](entities.md#soft-delete) for details.
145+
144146
### DeleteAll
145147

146148
```pascal
@@ -258,6 +260,21 @@ Validation also runs automatically before every `Insert` and `Update` operation
258260
| `InTransaction` | `Boolean` | Whether the write connection has an active transaction |
259261
| `SupportTransaction` | `Boolean` | Whether the write connection supports transactions |
260262
| `UseIdentityMap` | `Boolean` | Whether the identity map is enabled for this context |
263+
| `OnGetCurrentUser` | `TFunc<String>` | Callback that returns the current user name for change tracking `*By` fields |
264+
265+
### OnGetCurrentUser
266+
267+
Assign this property to provide the current user name for change tracking attributes (`[TCreatedBy]`, `[TUpdatedBy]`, `[TDeletedBy]`):
268+
269+
```pascal
270+
LContext.OnGetCurrentUser :=
271+
function: String
272+
begin
273+
Result := GetCurrentUserName;
274+
end;
275+
```
276+
277+
If not assigned, an empty string is written to `*By` fields. See [Entity Mapping — Change Tracking](entities.md#change-tracking) for details.
261278

262279
## Typical Usage Pattern
263280

Docs/guide/entities.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,92 @@ TActiveUser = class
102102
- Parameters are **compile-time constants** only. Supported types: `String`, `Integer`, `Int64`, `Double`, `Boolean`, `TDateTime`.
103103
- For dynamic, runtime-constructed filters, use [TTFilterBuilder\<T\>](filtering.md) instead.
104104

105+
## Change Tracking
106+
107+
Trysil can automatically set timestamp and user-name fields when entities are inserted, updated, or soft-deleted. Decorate columns with the change tracking attributes:
108+
109+
| Attribute | Set on | Required field type |
110+
|---|---|---|
111+
| `TCreatedAt` | Insert | `TTNullable<TDateTime>` |
112+
| `TCreatedBy` | Insert | `String` |
113+
| `TUpdatedAt` | Update | `TTNullable<TDateTime>` |
114+
| `TUpdatedBy` | Update | `String` |
115+
| `TDeletedAt` | Delete (soft) | `TTNullable<TDateTime>` |
116+
| `TDeletedBy` | Delete (soft) | `String` |
117+
118+
```pascal
119+
[TTable('Articles')]
120+
[TSequence('ArticlesID')]
121+
TArticle = class
122+
strict private
123+
[TPrimaryKey]
124+
[TColumn('ID')]
125+
FID: TTPrimaryKey;
126+
127+
[TColumn('Title')]
128+
FTitle: String;
129+
130+
[TCreatedAt]
131+
[TColumn('CreatedAt')]
132+
FCreatedAt: TTNullable<TDateTime>;
133+
134+
[TCreatedBy]
135+
[TColumn('CreatedBy')]
136+
FCreatedBy: String;
137+
138+
[TUpdatedAt]
139+
[TColumn('UpdatedAt')]
140+
FUpdatedAt: TTNullable<TDateTime>;
141+
142+
[TUpdatedBy]
143+
[TColumn('UpdatedBy')]
144+
FUpdatedBy: String;
145+
146+
[TDeletedAt]
147+
[TColumn('DeletedAt')]
148+
FDeletedAt: TTNullable<TDateTime>;
149+
150+
[TDeletedBy]
151+
[TColumn('DeletedBy')]
152+
FDeletedBy: String;
153+
154+
[TVersionColumn]
155+
[TColumn('VersionID')]
156+
FVersionID: TTVersion;
157+
public
158+
property ID: TTPrimaryKey read FID;
159+
property Title: String read FTitle write FTitle;
160+
end;
161+
```
162+
163+
### How It Works
164+
165+
- The resolver automatically populates `*At` fields with `Now` and `*By` fields with the value returned by `TTContext.OnGetCurrentUser` (empty string if not assigned).
166+
- `[TCreatedAt]` / `[TCreatedBy]` are set during `Insert`.
167+
- `[TUpdatedAt]` / `[TUpdatedBy]` are set during `Update`.
168+
- `[TDeletedAt]` / `[TDeletedBy]` are set during `Delete`.
169+
170+
### Soft Delete
171+
172+
When an entity has a `[TDeletedAt]` column, calling `Delete<T>` does **not** execute a SQL `DELETE`. Instead, it executes an `UPDATE` that sets the `DeletedAt` (and optionally `DeletedBy`) column and increments `[TVersionColumn]` if present. Relation checks (`TRelation`) are skipped for soft deletes.
173+
174+
All SELECT queries automatically add `DeletedAt IS NULL` to the WHERE clause, so soft-deleted records are excluded by default. To include them, use `TTFilter.IncludeDeleted` or `TTFilterBuilder<T>.IncludeDeleted` — see [Filtering](filtering.md#including-soft-deleted-records).
175+
176+
### Providing the Current User
177+
178+
Set `OnGetCurrentUser` on the context to supply the user name for `*By` fields:
179+
180+
```pascal
181+
LContext := TTContext.Create(LConnection);
182+
LContext.OnGetCurrentUser :=
183+
function: String
184+
begin
185+
Result := GetCurrentUserName; // your application logic
186+
end;
187+
```
188+
189+
If `OnGetCurrentUser` is not assigned, an empty string is written to `*By` fields.
190+
105191
## RTTI Warning
106192

107193
Always add this compiler directive at the top of units that define entities with Trysil attributes:

Docs/guide/filtering.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,32 @@ LBuilder
122122

123123
Only one `OrderBy` call is active at a time. Calling `OrderByAsc` or `OrderByDesc` replaces any previous ordering.
124124

125+
## Including Soft-Deleted Records
126+
127+
When an entity has a `[TDeletedAt]` column, all queries automatically exclude soft-deleted records by adding `DeletedAt IS NULL` to the WHERE clause. To include them:
128+
129+
### Via TTFilterBuilder
130+
131+
```pascal
132+
var LFilter := LContext.CreateFilterBuilder<TArticle>()
133+
.Where('Title').Like('Draft%')
134+
.IncludeDeleted
135+
.Build;
136+
137+
LContext.Select<TArticle>(LArticles, LFilter);
138+
```
139+
140+
### Via TTFilter
141+
142+
```pascal
143+
LFilter := TTFilter.Create('Title LIKE :Title');
144+
LFilter.AddParameter('Title', ftWideString, 'Draft%');
145+
LFilter.IncludeDeleted := True;
146+
LContext.Select<TArticle>(LArticles, LFilter);
147+
```
148+
149+
See [Entity Mapping — Soft Delete](entities.md#soft-delete) for how to set up change tracking attributes.
150+
125151
## SelectCount
126152

127153
Count records matching a filter without loading entities:

Readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- **4 database drivers** — SQLite, PostgreSQL, SQL Server, Firebird — all through FireDAC
2525
- **Fluent query builder** — type-safe filtering with `TTFilterBuilder<T>`
2626
- **Lazy loading**`TTLazy<T>` and `TTLazyList<T>` for related entities
27+
- **Change tracking & soft delete**`[TCreatedAt]`, `[TUpdatedAt]`, `[TDeletedAt]` with automatic timestamps and user tracking
2728
- **Optimistic locking** — built-in via `[TVersionColumn]`
2829
- **Identity map** — per-context, multi-tenant safe
2930
- **Unit of Work**`TTSession<T>` tracks and applies changes automatically

0 commit comments

Comments
 (0)