Tina4

Chapter 8: Core Utilities#

The Swiss Army Knife You Already Have#

Every Delphi project accumulates a Utils.pas file. String conversion functions. Date formatting helpers. JSON parsing wrappers. HTTP request boilerplate. File encoding routines. You write them once, copy them across projects, fix bugs in three places, forget to fix the fourth.

Tina4Core.pas is that utils file, already written and tested. Add it to your uses clause and you get string helpers, GUID generation, date utilities, Base64 encoding, JSON parsing, database-to-JSON conversion, JSON-to-MemTable population, HTTP requests, multipart file uploads, and shell command execution. No components to drop on a form. No configuration. Just functions.

pascal
uses  Tina4Core;

That single line gives you everything in this chapter.


1. String Helpers#

CamelCase#

Converts snake_case to camelCase. This is the function that makes database field names feel natural in JSON:

pascal
CamelCase('first_name');      // 'firstName'CamelCase('id');              // 'id'CamelCase('user_email');      // 'userEmail'CamelCase('created_at');      // 'createdAt'CamelCase('order_line_item'); // 'orderLineItem'

GetJSONFromDB uses this internally. When your database table has first_name and last_name columns, the JSON output automatically uses firstName and lastName.

SnakeCase#

The reverse -- converts camelCase back to snake_case. Used when mapping JSON keys back to database columns:

pascal
SnakeCase('firstName');      // 'first_name'SnakeCase('userEmail');      // 'user_email'SnakeCase('createdAt');      // 'created_at'SnakeCase('orderLineItem');  // 'order_line_item'

When You Need Both#

pascal
// API sends camelCase JSON, database uses snake_case columnsvar JSONKey := 'orderTotal';var DBColumn := SnakeCase(JSONKey);  // 'order_total'โ€‹// Query returns snake_case, API expects camelCasevar DBField := 'shipping_address';var APIField := CamelCase(DBField);  // 'shippingAddress'

2. GUID Generation#

GetGUID#

Returns a new GUID string without braces:

pascal
var ID := GetGUID;// e.g. 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890'โ€‹// Use as a primary keyFDQuery1.SQL.Text := 'INSERT INTO documents (id, title) VALUES (:id, :title)';FDQuery1.ParamByName('id').AsString := GetGUID;FDQuery1.ParamByName('title').AsString := 'New Document';FDQuery1.ExecSQL;

Each call produces a unique value. Use it for primary keys, session tokens, file names, or any situation that needs a globally unique identifier.

pascal
// Generate unique filenamesvar FileName := GetGUID + '.pdf';โ€‹// Create a batch ID for grouped operationsvar BatchID := GetGUID;for var I := 0 to Items.Count - 1 dobegin  FDQuery1.SQL.Text := 'INSERT INTO batch_items (batch_id, item_id) VALUES (:batch, :item)';  FDQuery1.ParamByName('batch').AsString := BatchID;  FDQuery1.ParamByName('item').AsString := Items[I];  FDQuery1.ExecSQL;end;

3. Date Utilities#

IsDate#

Checks whether a Variant value represents a valid date. Supports multiple formats:

pascal
IsDate('2024-01-15');                  // True - ISO dateIsDate('2024-01-15T10:30:00');         // True - ISO datetimeIsDate('2024-01-15T10:30:00.000Z');    // True - ISO with millisecondsIsDate('01/15/2024');                  // True - US date formatIsDate('2024-01-15 14:30:00');         // True - datetime with spaceIsDate('42');                          // False - numberIsDate('hello');                       // False - stringIsDate('');                            // False - empty

Use it to validate user input or incoming API data:

pascal
procedure TForm1.ValidateDateField(const AValue: string);begin  if not IsDate(AValue) then    raise Exception.Create('Invalid date format: ' + AValue);end;

GetJSONDate#

Converts a TDateTime to an ISO 8601 string -- the standard format for JSON APIs:

pascal
GetJSONDate(Now);// '2026-03-26T14:30:00.000Z'โ€‹GetJSONDate(EncodeDate(2026, 1, 1));// '2026-01-01T00:00:00.000Z'

Use it when building JSON payloads:

pascal
var Order := TJSONObject.Create;try  Order.AddPair('id', GetGUID);  Order.AddPair('createdAt', GetJSONDate(Now));  Order.AddPair('total', TJSONNumber.Create(99.99));  // Send to API...finally  Order.Free;end;

JSONDateToDateTime#

Converts an ISO 8601 date string back to a TDateTime:

pascal
var DT := JSONDateToDateTime('2026-03-26T14:30:00.000Z');// DT is now a TDateTime you can use with FormatDateTime, DateUtils, etc.โ€‹ShowMessage(FormatDateTime('dd/mm/yyyy hh:nn', DT));// '26/03/2026 14:30'

The round-trip works cleanly:

pascal
var Original := Now;var JSON := GetJSONDate(Original);var Restored := JSONDateToDateTime(JSON);// Original and Restored represent the same point in time

4. Encoding#

DecodeBase64#

Decodes a Base64-encoded string back to plain UTF-8:

pascal
DecodeBase64('SGVsbG8gV29ybGQ=');// 'Hello World'โ€‹// Decode an API tokenvar Token := DecodeBase64(EncodedToken);

FileToBase64#

Reads an entire file and returns its content as a Base64 string. Works with any file type:

pascal
var B64 := FileToBase64('C:\photos\avatar.jpg');// B64 now contains the Base64-encoded JPEG dataโ€‹// Embed in JSON for API uploadvar Payload := TJSONObject.Create;try  Payload.AddPair('filename', 'avatar.jpg');  Payload.AddPair('data', FileToBase64('C:\photos\avatar.jpg'));  Payload.AddPair('mimeType', 'image/jpeg');  // POST to API...finally  Payload.Free;end;

BitmapToBase64EncodedString#

Encodes an FMX TBitmap to a Base64 string with optional resizing. The default resize is 256x256 pixels -- ideal for thumbnails and avatars:

pascal
// Default: resize to 256x256var Encoded := BitmapToBase64EncodedString(Image1.Bitmap);โ€‹// No resize -- keep original dimensionsvar Encoded := BitmapToBase64EncodedString(Image1.Bitmap, False);โ€‹// Custom resizevar Encoded := BitmapToBase64EncodedString(Image1.Bitmap, True, 128, 128);var Encoded := BitmapToBase64EncodedString(Image1.Bitmap, True, 512, 512);

Practical use -- capture and upload a profile photo:

pascal
procedure TForm1.ButtonUploadClick(Sender: TObject);var  StatusCode: Integer;  Encoded: string;begin  Encoded := BitmapToBase64EncodedString(ImageAvatar.Bitmap, True, 200, 200);โ€‹  var Body := TJSONObject.Create;  try    Body.AddPair('userId', '1001');    Body.AddPair('avatar', Encoded);โ€‹    SendHttpRequest(StatusCode,      'https://api.example.com', '/users/1001/avatar', '',      Body.ToString, 'application/json', 'utf-8', '', '', nil, 'Tina4Delphi',      TTina4RequestType.Patch);โ€‹    if StatusCode = 200 then      ShowMessage('Avatar uploaded')    else      ShowMessage('Upload failed: ' + StatusCode.ToString);  finally    Body.Free;  end;end;

BitmapToSkiaWepPEncodedString#

Encodes an FMX TBitmap to a WebP Base64 string using Skia. Requires the SKIA compiler define. WebP produces smaller files than JPEG at the same quality:

pascal
{$IFDEF SKIA}var WebPData := BitmapToSkiaWepPEncodedString(Image1.Bitmap, 90);// quality parameter: 0-100 (higher = better quality, larger size){$ENDIF}

5. JSON Parsing#

StrToJSONObject#

Parses a JSON string into a TJSONObject. Returns nil if parsing fails -- always check:

pascal
var Obj := StrToJSONObject('{"name": "Andre", "age": 30}');try  if Assigned(Obj) then  begin    var Name := Obj.GetValue<String>('name');   // 'Andre'    var Age := Obj.GetValue<Integer>('age');     // 30    ShowMessage(Name + ' is ' + Age.ToString);  end  else    ShowMessage('Invalid JSON');finally  Obj.Free;end;

StrToJSONArray#

Parses a JSON array string:

pascal
var Arr := StrToJSONArray('[1, 2, 3, 4, 5]');try  if Assigned(Arr) then    for var I := 0 to Arr.Count - 1 do      ShowMessage(Arr.Items[I].Value);finally  Arr.Free;end;

StrToJSONValue#

Parses any JSON value -- object, array, string, number, boolean, or null. Use when you do not know the structure in advance:

pascal
var Val := StrToJSONValue(APIResponse);if Val is TJSONObject then  // Handle objectelse if Val is TJSONArray then  // Handle arrayelse if Val is TJSONString then  // Handle string

BytesToJSONObject#

Parses a TBytes buffer into a TJSONObject. This is the bridge between raw HTTP responses and structured JSON:

pascal
var  StatusCode: Integer;  Response: TBytes;begin  Response := SendHttpRequest(StatusCode, 'https://api.example.com', '/users');โ€‹  var JSON := BytesToJSONObject(Response);  try    if Assigned(JSON) then    begin      // Process the response      var Records := JSON.GetValue<TJSONArray>('records');      ShowMessage('Found ' + Records.Count.ToString + ' users');    end;  finally    JSON.Free;  end;end;

GetJSONFieldName#

Strips surrounding quotes from a JSON field name string:

pascal
GetJSONFieldName('"firstName"');  // 'firstName'GetJSONFieldName('"id"');         // 'id'GetJSONFieldName('name');         // 'name' (no change)

6. Database to JSON#

GetJSONFromDB#

Executes a SQL query and returns the results as a TJSONObject. Three automatic conversions happen:

  1. Field names are converted from snake_case to camelCase
  2. DateTime fields are formatted as ISO 8601
  3. Blob fields are encoded as Base64
pascal
// Simple queryvar Result := GetJSONFromDB(FDConnection1, 'SELECT * FROM users');// {"records": [{"id": "1", "firstName": "Andre", "email": "andre@test.com"}, ...]}โ€‹// Custom dataset keyvar Result := GetJSONFromDB(FDConnection1, 'SELECT * FROM products', nil, 'products');// {"products": [{"id": "1", "productName": "Widget"}, ...]}

With Parameters#

pascal
var Params := TFDParams.Create;try  Params.Add('status', 'active');  Params.Add('minAge', 18);โ€‹  var Result := GetJSONFromDB(FDConnection1,    'SELECT * FROM users WHERE status = :status AND age >= :minAge',    Params);  try    Memo1.Lines.Text := Result.Format(2);  finally    Result.Free;  end;finally  Params.Free;end;

Serving JSON from a Database#

Combine GetJSONFromDB with an HTTP response to build an instant API:

pascal
procedure TForm1.HandleGetUsers;var  Result: TJSONObject;begin  Result := GetJSONFromDB(FDConnection1,    'SELECT id, first_name, last_name, email, created_at ' +    'FROM users WHERE active = 1 ORDER BY last_name');  try    // Result is ready to send as an API response:    // {    //   "records": [    //     {"id": "1", "firstName": "Andre", "lastName": "Van Zuydam",    //      "email": "andre@test.com", "createdAt": "2026-03-15T10:30:00.000Z"},    //     ...    //   ]    // }    Memo1.Lines.Text := Result.Format(2);  finally    Result.Free;  end;end;

GetJSONFromTable#

Converts rows in a TFDMemTable or TFDTable to JSON:

pascal
// Basic conversionvar JSON := GetJSONFromTable(FDMemTable1);// {"records": [{"id": "1", "name": "Item 1"}, ...]}โ€‹// Ignore specific fields (e.g., sensitive data)var JSON := GetJSONFromTable(FDMemTable1, 'records', 'password,secret_key');โ€‹// Ignore blank values (smaller JSON)var JSON := GetJSONFromTable(FDMemTable1, 'records', '', True);

7. JSON to MemTable#

GetFieldDefsFromJSONObject#

Creates field definitions on a TFDMemTable from a TJSONObject structure. Call this before populating the table if you need auto-created fields:

pascal
var JSONObj := StrToJSONObject(  '{"firstName": "Andre", "age": 30, "address": {"city": "Cape Town"}}');try  GetFieldDefsFromJSONObject(JSONObj, FDMemTable1, True);  // Creates fields:  //   first_name (ftString)  -- camelCase converted to snake_case  //   age (ftString)  //   address (ftMemo)       -- nested objects become memo fieldsfinally  JSONObj.Free;end;

The third parameter controls snake_case conversion:

pascal
// With snake_case conversion (True) -- good for database-style field namesGetFieldDefsFromJSONObject(JSONObj, MemTable, True);// firstName -> first_nameโ€‹// Without conversion (False) -- keeps camelCaseGetFieldDefsFromJSONObject(JSONObj, MemTable, False);// firstName -> firstName

PopulateMemTableFromJSON#

Populates a TFDMemTable from a JSON string. Two sync modes control how existing data is handled:

Clear mode (default) -- empties the table, then appends all records:

pascal
PopulateMemTableFromJSON(FDMemTable1, 'records',  '{"records": [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]}');// FDMemTable1 now has exactly 2 records

Sync mode -- matches existing records by a key field, updates them, and inserts new ones:

pascal
// First loadPopulateMemTableFromJSON(FDMemTable1, 'records',  '{"records": [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]}');โ€‹// Later: sync with updated dataPopulateMemTableFromJSON(FDMemTable1, 'records',  '{"records": [{"id": "1", "name": "Alice Updated"}, {"id": "3", "name": "Charlie"}]}',  'id', TTina4RestSyncMode.Sync);// FDMemTable1 now has 3 records://   id=1: "Alice Updated" (updated)//   id=2: "Bob" (unchanged)//   id=3: "Charlie" (inserted)

PopulateTableFromJSON#

Inserts or updates rows directly into a database table (not a MemTable) from JSON. Uses a primary key for upsert logic:

pascal
var Result := PopulateTableFromJSON(  FDConnection1,  'users',                    // table name  '{"response": [{"name": "Alice"}, {"name": "Bob"}]}',  'response',                 // JSON key containing the array  'id');                      // primary key field for upsertโ€‹// Rows are inserted or updated directly in the 'users' table

This is the fastest way to sync remote API data into a local database.


8. HTTP Requests#

SendHttpRequest#

Low-level HTTP function that returns raw TBytes. Supports GET, POST, PATCH, PUT, and DELETE:

pascal
var  StatusCode: Integer;  Response: TBytes;begin  // Simple GET  Response := SendHttpRequest(StatusCode,    'https://api.example.com', '/users');โ€‹  if StatusCode = 200 then  begin    var JSON := BytesToJSONObject(Response);    try      // Process response...    finally      JSON.Free;    end;  end;end;

POST with JSON Body#

pascal
var  StatusCode: Integer;  Body: string;begin  Body := '{"name": "Andre", "email": "andre@test.com"}';โ€‹  SendHttpRequest(StatusCode,    'https://api.example.com',   // base URL    '/users',                     // endpoint    '',                           // query params    Body,                         // request body    'application/json',           // content type    'utf-8',                      // charset    '', '',                       // username, password (for Basic Auth)    nil,                          // custom headers    'Tina4Delphi',               // user agent    TTina4RequestType.Post);      // request typeโ€‹  case StatusCode of    201: ShowMessage('User created');    400: ShowMessage('Bad request');    401: ShowMessage('Unauthorized');    500: ShowMessage('Server error');  end;end;

With Basic Auth#

pascal
Response := SendHttpRequest(StatusCode,  'https://api.example.com', '/secure/data',  '', '',  'application/json', 'utf-8',  'myuser', 'mypassword');

PATCH and DELETE#

pascal
// Update a userSendHttpRequest(StatusCode,  'https://api.example.com', '/users/1001', '',  '{"name": "Andre Updated"}',  'application/json', 'utf-8', '', '', nil, 'Tina4Delphi',  TTina4RequestType.Patch);โ€‹// Delete a userSendHttpRequest(StatusCode,  'https://api.example.com', '/users/1001', '',  '', 'application/json', 'utf-8', '', '', nil, 'Tina4Delphi',  TTina4RequestType.Delete);

SendMultipartFormData#

Sends a multipart/form-data POST for file uploads with optional form fields:

pascal
var  StatusCode: Integer;  Response: TBytes;begin  Response := SendMultipartFormData(    StatusCode,    'https://api.example.com',      // base URL    'upload/avatar',                 // endpoint    ['userId', '1001',              // form fields (key-value pairs)     'caption', 'Profile photo'],    ['avatar', 'C:\photos\me.jpg'], // files (field name, file path)    '',                              // query params    'myuser', 'mypassword');         // authโ€‹  if StatusCode = 200 then    ShowMessage('Upload successful');end;

Multiple files:

pascal
Response := SendMultipartFormData(  StatusCode,  'https://api.example.com', 'upload/documents',  ['projectId', '42'],  ['doc1', 'C:\docs\spec.pdf',   'doc2', 'C:\docs\design.pdf',   'doc3', 'C:\docs\timeline.xlsx'],  '', '', '');

9. Shell Commands#

ExecuteShellCommand#

Runs a shell command and captures stdout. Works on Windows, Linux, and macOS:

pascal
var  Output: String;  ExitCode: Integer;begin  // Windows  ExitCode := ExecuteShellCommand('dir C:\temp', Output);  ShowMessage(Output);โ€‹  // macOS / Linux  ExitCode := ExecuteShellCommand('ls -la /tmp', Output);  ShowMessage(Output);end;

Check the exit code:

pascal
var  Output: String;  ExitCode: Integer;begin  ExitCode := ExecuteShellCommand('ping -n 4 google.com', Output);โ€‹  if ExitCode = 0 then    ShowMessage('Ping successful:' + sLineBreak + Output)  else    ShowMessage('Ping failed with exit code: ' + ExitCode.ToString);end;

Cross-Platform Commands#

pascal
procedure TForm1.RunCommand(const ACommand: string);var  Output: String;  ExitCode: Integer;begin  ExitCode := ExecuteShellCommand(ACommand, Output);  MemoOutput.Lines.Text := Output;  LabelExitCode.Text := 'Exit code: ' + ExitCode.ToString;end;โ€‹// Usage:{$IFDEF MSWINDOWS}RunCommand('ipconfig /all');{$ELSE}RunCommand('ifconfig');{$ENDIF}

10. Complete Example: File Upload Utility#

Build a utility that selects an image, resizes it, encodes to Base64, uploads via multipart form data, and displays the server response.

pascal
unit UploadForm;โ€‹interfaceโ€‹uses  System.SysUtils, System.Types, System.Classes, System.JSON,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Objects,  FMX.Dialogs, FMX.Layouts, FMX.Memo,  Tina4Core;โ€‹type  TFormUpload = class(TForm)    ImagePreview: TImage;    ButtonSelect: TButton;    ButtonUpload: TButton;    LabelStatus: TLabel;    MemoResponse: TMemo;    OpenDialog1: TOpenDialog;    LabelFileInfo: TLabel;    procedure ButtonSelectClick(Sender: TObject);    procedure ButtonUploadClick(Sender: TObject);  private    FSelectedFile: string;    procedure UpdateFileInfo;  end;โ€‹var  FormUpload: TFormUpload;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TFormUpload.ButtonSelectClick(Sender: TObject);begin  OpenDialog1.Filter := 'Image files|*.jpg;*.jpeg;*.png;*.bmp|All files|*.*';โ€‹  if OpenDialog1.Execute then  begin    FSelectedFile := OpenDialog1.FileName;    ImagePreview.Bitmap.LoadFromFile(FSelectedFile);    UpdateFileInfo;    ButtonUpload.Enabled := True;    LabelStatus.Text := 'Image selected. Ready to upload.';  end;end;โ€‹procedure TFormUpload.UpdateFileInfo;var  FileSize: Int64;  Info: TSearchRec;begin  if FindFirst(FSelectedFile, faAnyFile, Info) = 0 then  begin    FileSize := Info.Size;    FindClose(Info);โ€‹    LabelFileInfo.Text := Format('File: %s | Size: %s | Dimensions: %dx%d',      [ExtractFileName(FSelectedFile),       FormatFloat('#,##0', FileSize) + ' bytes',       Round(ImagePreview.Bitmap.Width),       Round(ImagePreview.Bitmap.Height)]);  end;end;โ€‹procedure TFormUpload.ButtonUploadClick(Sender: TObject);var  StatusCode: Integer;  Response: TBytes;  Encoded: string;begin  if FSelectedFile.IsEmpty then  begin    ShowMessage('Please select an image first');    Exit;  end;โ€‹  LabelStatus.Text := 'Resizing image...';  Application.ProcessMessages;โ€‹  // Resize and encode to Base64  Encoded := BitmapToBase64EncodedString(ImagePreview.Bitmap, True, 512, 512);โ€‹  LabelStatus.Text := Format('Encoded to Base64 (%d chars). Uploading...',    [Length(Encoded)]);  Application.ProcessMessages;โ€‹  // Method 1: Upload as multipart form data (file upload)  Response := SendMultipartFormData(    StatusCode,    'https://api.example.com',    'upload/image',    ['userId', '1001',     'description', 'Uploaded from Delphi'],    ['image', FSelectedFile],    '', '', '');โ€‹  if StatusCode in [200, 201] then  begin    LabelStatus.Text := 'Upload successful!';โ€‹    var JSON := BytesToJSONObject(Response);    try      if Assigned(JSON) then        MemoResponse.Lines.Text := JSON.Format(2)      else        MemoResponse.Lines.Text := TEncoding.UTF8.GetString(Response);    finally      JSON.Free;    end;  end  else  begin    LabelStatus.Text := 'Upload failed: HTTP ' + StatusCode.ToString;    MemoResponse.Lines.Text := TEncoding.UTF8.GetString(Response);  end;end;โ€‹end.

11. Complete Example: Database Sync Tool#

Fetch JSON from a remote API, compare with a local database, and sync changes using PopulateTableFromJSON.

pascal
unit SyncForm;โ€‹interfaceโ€‹uses  System.SysUtils, System.Types, System.Classes, System.JSON,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Memo,  FMX.Layouts, FMX.Dialogs,  FireDAC.Comp.Client, FireDAC.Stan.Def, FireDAC.Stan.Async,  FireDAC.Phys.SQLite, FireDAC.DApt,  Tina4Core;โ€‹type  TFormSync = class(TForm)    FDConnection1: TFDConnection;    FDMemTableLocal: TFDMemTable;    FDMemTableRemote: TFDMemTable;    ButtonSync: TButton;    ButtonCompare: TButton;    MemoLog: TMemo;    LabelStatus: TLabel;    procedure FormCreate(Sender: TObject);    procedure ButtonCompareClick(Sender: TObject);    procedure ButtonSyncClick(Sender: TObject);  private    procedure Log(const AMessage: string);    procedure FetchRemoteData;    procedure LoadLocalData;    procedure CompareAndReport;    procedure SyncToLocal;  end;โ€‹var  FormSync: TFormSync;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TFormSync.FormCreate(Sender: TObject);begin  // Setup SQLite connection  FDConnection1.Params.Clear;  FDConnection1.Params.Add('DriverID=SQLite');  FDConnection1.Params.Add('Database=C:\MyApp\data\local.db');  FDConnection1.Connected := True;โ€‹  // Ensure the products table exists  FDConnection1.ExecSQL(    'CREATE TABLE IF NOT EXISTS products (' +    '  id TEXT PRIMARY KEY,' +    '  name TEXT,' +    '  price REAL,' +    '  stock INTEGER,' +    '  updated_at TEXT' +    ')');โ€‹  Log('Database connected. Ready to sync.');end;โ€‹procedure TFormSync.Log(const AMessage: string);begin  MemoLog.Lines.Add(FormatDateTime('hh:nn:ss', Now) + '  ' + AMessage);end;โ€‹procedure TFormSync.FetchRemoteData;var  StatusCode: Integer;  Response: TBytes;  JSON: TJSONObject;begin  Log('Fetching remote data...');  LabelStatus.Text := 'Fetching from API...';  Application.ProcessMessages;โ€‹  Response := SendHttpRequest(StatusCode,    'https://api.example.com', '/products', 'limit=1000');โ€‹  if StatusCode <> 200 then  begin    Log('API error: HTTP ' + StatusCode.ToString);    Exit;  end;โ€‹  var JSONStr := TEncoding.UTF8.GetString(Response);โ€‹  // Populate remote MemTable  PopulateMemTableFromJSON(FDMemTableRemote, 'records', JSONStr);  Log('Fetched ' + FDMemTableRemote.RecordCount.ToString + ' remote products');end;โ€‹procedure TFormSync.LoadLocalData;var  Result: TJSONObject;begin  Log('Loading local data...');โ€‹  Result := GetJSONFromDB(FDConnection1, 'SELECT * FROM products');  try    if Assigned(Result) then    begin      PopulateMemTableFromJSON(FDMemTableLocal, 'records', Result.ToString);      Log('Loaded ' + FDMemTableLocal.RecordCount.ToString + ' local products');    end;  finally    Result.Free;  end;end;โ€‹procedure TFormSync.CompareAndReport;var  LocalCount, RemoteCount: Integer;  NewCount, UpdatedCount: Integer;begin  LocalCount := FDMemTableLocal.RecordCount;  RemoteCount := FDMemTableRemote.RecordCount;  NewCount := 0;  UpdatedCount := 0;โ€‹  // Check each remote record against local  FDMemTableRemote.First;  while not FDMemTableRemote.Eof do  begin    var RemoteID := FDMemTableRemote.FieldByName('id').AsString;โ€‹    if FDMemTableLocal.Locate('id', RemoteID) then    begin      // Check if updated      var RemoteName := FDMemTableRemote.FieldByName('name').AsString;      var LocalName := FDMemTableLocal.FieldByName('name').AsString;      if RemoteName <> LocalName then        Inc(UpdatedCount);    end    else      Inc(NewCount);โ€‹    FDMemTableRemote.Next;  end;โ€‹  Log('---');  Log('Comparison Results:');  Log('  Local records:  ' + LocalCount.ToString);  Log('  Remote records: ' + RemoteCount.ToString);  Log('  New records:    ' + NewCount.ToString);  Log('  Changed records: ' + UpdatedCount.ToString);  Log('---');end;โ€‹procedure TFormSync.ButtonCompareClick(Sender: TObject);begin  FetchRemoteData;  LoadLocalData;  CompareAndReport;  LabelStatus.Text := 'Comparison complete';end;โ€‹procedure TFormSync.ButtonSyncClick(Sender: TObject);begin  FetchRemoteData;  SyncToLocal;  LoadLocalData;  // Reload to verify  LabelStatus.Text := 'Sync complete';end;โ€‹procedure TFormSync.SyncToLocal;var  StatusCode: Integer;  Response: TBytes;begin  Log('Syncing remote data to local database...');  LabelStatus.Text := 'Syncing...';  Application.ProcessMessages;โ€‹  // Fetch fresh data  Response := SendHttpRequest(StatusCode,    'https://api.example.com', '/products', 'limit=1000');โ€‹  if StatusCode <> 200 then  begin    Log('Sync failed: HTTP ' + StatusCode.ToString);    Exit;  end;โ€‹  var JSONStr := TEncoding.UTF8.GetString(Response);โ€‹  // Upsert into the database table  var Result := PopulateTableFromJSON(    FDConnection1,    'products',     // table name    JSONStr,        // JSON data    'records',      // JSON array key    'id');          // primary key for upsertโ€‹  Log('Sync complete. Processed records.');end;โ€‹end.

12. Exercise: CLI Wrapper#

Build a command-line wrapper that executes shell commands from a Delphi form, captures and displays output, and supports common operations.

Requirements#

  1. A text input for custom commands
  2. Quick-action buttons for common operations: Ping, Traceroute, Directory listing, IP configuration
  3. Output display with timestamps
  4. Cross-platform command detection (Windows vs macOS/Linux)
  5. Exit code display

Solution#

pascal
unit CLIForm;โ€‹interfaceโ€‹uses  System.SysUtils, System.Types, System.Classes,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Edit,  FMX.Memo, FMX.Layouts,  Tina4Core;โ€‹type  TFormCLI = class(TForm)    EditCommand: TEdit;    ButtonRun: TButton;    ButtonPing: TButton;    ButtonTrace: TButton;    ButtonDir: TButton;    ButtonIPConfig: TButton;    ButtonClear: TButton;    MemoOutput: TMemo;    LabelExitCode: TLabel;    LabelPlatform: TLabel;    procedure FormCreate(Sender: TObject);    procedure ButtonRunClick(Sender: TObject);    procedure ButtonPingClick(Sender: TObject);    procedure ButtonTraceClick(Sender: TObject);    procedure ButtonDirClick(Sender: TObject);    procedure ButtonIPConfigClick(Sender: TObject);    procedure ButtonClearClick(Sender: TObject);    procedure EditCommandKeyDown(Sender: TObject; var Key: Word;      var KeyChar: Char; Shift: TShiftState);  private    procedure RunCommand(const ACommand: string);    function IsWindows: Boolean;  end;โ€‹var  FormCLI: TFormCLI;โ€‹implementationโ€‹{$R *.fmx}โ€‹function TFormCLI.IsWindows: Boolean;begin  {$IFDEF MSWINDOWS}  Result := True;  {$ELSE}  Result := False;  {$ENDIF}end;โ€‹procedure TFormCLI.FormCreate(Sender: TObject);begin  if IsWindows then    LabelPlatform.Text := 'Platform: Windows'  else    LabelPlatform.Text := 'Platform: macOS / Linux';end;โ€‹procedure TFormCLI.RunCommand(const ACommand: string);var  Output: String;  ExitCode: Integer;begin  MemoOutput.Lines.Add('');  MemoOutput.Lines.Add('=== ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' ===');  MemoOutput.Lines.Add('$ ' + ACommand);  MemoOutput.Lines.Add('');โ€‹  LabelExitCode.Text := 'Running...';  Application.ProcessMessages;โ€‹  ExitCode := ExecuteShellCommand(ACommand, Output);โ€‹  MemoOutput.Lines.Add(Output);  MemoOutput.Lines.Add('');โ€‹  if ExitCode = 0 then    LabelExitCode.Text := 'Exit code: 0 (success)'  else    LabelExitCode.Text := 'Exit code: ' + ExitCode.ToString + ' (error)';โ€‹  // Scroll to bottom  MemoOutput.GoToTextEnd;end;โ€‹procedure TFormCLI.ButtonRunClick(Sender: TObject);begin  if EditCommand.Text.Trim.IsEmpty then  begin    ShowMessage('Enter a command to run');    Exit;  end;โ€‹  RunCommand(EditCommand.Text.Trim);end;โ€‹procedure TFormCLI.EditCommandKeyDown(Sender: TObject; var Key: Word;  var KeyChar: Char; Shift: TShiftState);begin  if Key = vkReturn then    ButtonRunClick(Sender);end;โ€‹procedure TFormCLI.ButtonPingClick(Sender: TObject);begin  if IsWindows then    RunCommand('ping -n 4 google.com')  else    RunCommand('ping -c 4 google.com');end;โ€‹procedure TFormCLI.ButtonTraceClick(Sender: TObject);begin  if IsWindows then    RunCommand('tracert google.com')  else    RunCommand('traceroute google.com');end;โ€‹procedure TFormCLI.ButtonDirClick(Sender: TObject);begin  if IsWindows then    RunCommand('dir')  else    RunCommand('ls -la');end;โ€‹procedure TFormCLI.ButtonIPConfigClick(Sender: TObject);begin  if IsWindows then    RunCommand('ipconfig')  else    RunCommand('ifconfig');end;โ€‹procedure TFormCLI.ButtonClearClick(Sender: TObject);begin  MemoOutput.Lines.Clear;  LabelExitCode.Text := '';end;โ€‹end.

Common Gotchas#

TJSONObject memory leaks. Every TJSONObject, TJSONArray, or TJSONValue you create owns its children. When you call Free on the parent, all children are freed too. But if you create one and forget to free it, you leak memory. The pattern is always try/finally:

pascal
var Obj := StrToJSONObject(SomeString);if Assigned(Obj) thentry  // Use Obj...finally  Obj.Free;end;

Do NOT free children of a TJSONObject separately -- the parent owns them:

pascal
// Wrong -- double freevar Obj := StrToJSONObject('{"items": [1,2,3]}');var Arr := Obj.GetValue<TJSONArray>('items');Arr.Free;   // CRASH! Obj owns ArrObj.Free;โ€‹// Rightvar Obj := StrToJSONObject('{"items": [1,2,3]}');try  var Arr := Obj.GetValue<TJSONArray>('items');  // Use Arr... don't free itfinally  Obj.Free;  // Frees everythingend;

TBytes vs String conversion. HTTP responses come back as TBytes. To get a string:

pascal
var Text := TEncoding.UTF8.GetString(Response);

To go the other way:

pascal
var Bytes := TEncoding.UTF8.GetBytes(MyString);

Never assume ASCII. Always use TEncoding.UTF8.

Cross-platform shell commands. ExecuteShellCommand works differently on Windows and macOS/Linux. Windows uses cmd.exe, macOS/Linux uses /bin/sh. Always use conditional compilation for platform-specific commands:

pascal
{$IFDEF MSWINDOWS}ExitCode := ExecuteShellCommand('dir /b C:\temp', Output);{$ELSE}ExitCode := ExecuteShellCommand('ls /tmp', Output);{$ENDIF}

GetJSONFromDB field name conversion. The automatic snake_case to camelCase conversion is helpful, but it means your JSON keys will not match your database column names. If you need exact column names in your JSON, use GetJSONFromTable which does not convert names by default.

PopulateMemTableFromJSON field matching. When using Sync mode, the IndexFieldNames must be set on the MemTable before calling PopulateMemTableFromJSON. If the field names do not match (camelCase vs snake_case), sync will insert duplicates instead of updating:

pascal
// Set the index field BEFORE populatingFDMemTable1.IndexFieldNames := 'id';โ€‹// Now sync works correctlyPopulateMemTableFromJSON(FDMemTable1, 'records', JSONData,  'id', TTina4RestSyncMode.Sync);

SendHttpRequest timeout. The default timeout is system-dependent. For slow APIs or large uploads, you may get a timeout before the request completes. Pass a timeout parameter when available, or use TTina4RESTRequest with async execution for long-running requests.