TMMConnection, TMMScript, and
TMMUpdater are TComponent-based classes.
Each TMMScript and TMMUpdater owns its own
HTTP client and is fully independent of any other instance.
Use one TMMConnection per thread and share it across as
many TMMScript and TMMUpdater instances as
needed.
The library exposes two main operations:
TMMScript.Execute — execute SQL on the
remote server and materialise the result set locally via an EDB
temporary table.TMMUpdater.Prepare +
Execute — write rows from a local EDB buffer to a
remote MySQL/MariaDB table via batch INSERT, UPDATE, or UPSERT.Execute performs the complete upload–execute–materialise
cycle:
SQL to the server.uses EDBMMClient; // TMMConnection, TMMScript, TMMUpdater
// uses UErrorCodes; // add when checking EC_* constants directly
var Script: TMMScript;
Script := TMMScript.Create(nil);
try
Script.Connection := Conn;
Script.SQL.Text := 'SELECT id, name FROM orders WHERE status = ''pending''';
Script.Execute;
if not Script.Success then
begin
ShowMessage('Error: ' + Script.LastError.Msg);
Exit;
end;
if Script.HasResultSet then
begin
// Script.Table is open and ready; iterate directly
while not Script.Table.EOF do
begin
ShowMessage(Script.Table.FieldByName('name').AsString);
Script.Table.Next;
end;
end;
finally
FreeAndNil(Script); // destructor drops the EDB temp table
end;
Re-calling Execute drops the previous result and
replaces it.
The caller owns Script and frees it; the destructor drops
the EDB temp table automatically.
Multi-statement scripts are supported; only the result set of the last statement is returned.
DDL statements (
CREATE TABLE,ALTER TABLE, etc.) cause an implicit commit in MySQL/MariaDB and cannot be rolled back even inside a transaction. WhenUseTransaction = Trueand an error occurs,Script.RolledBackis set toTrue.
| Property | Type | Description |
|---|---|---|
Success |
Boolean | True when execution succeeded |
LastError |
TAppError |
Structured error; Code = EC_NONE on success |
HasResultSet |
Boolean | True when the last statement produced rows |
ResultIncomplete |
Boolean | True when the server aborted the fetch early (PHP OOM
or MySQL timeout), or when a later result page failed to load into the
EDB table; partial result is valid. Reset to False at the
start of each Execute call. |
TempTableName |
string | Local EDB temp table name; non-empty when
HasResultSet = True |
RowCount |
Int64 | Total rows in the result set |
StatementsExec |
Integer | Number of SQL statements executed |
ExecutionTimeSec |
Double | Server-side execution time in seconds |
RolledBack |
Boolean | True when a transaction was rolled back by the
server |
Cancelled |
Boolean | True when Cancel was called during
execution |
ResultTableDDL |
string | CREATE TABLE DDL used to build the EDB result
table |
Columns |
TMMColumnInfoArray |
Per-column metadata array; indexed parallel to the result fields.
Includes SourceName, SourceTable,
IsBinary, PrimaryKeyIndex, Scale
and all the detailed definition of each column |
By default TMMScript creates a TEDBTable
internally and exposes it via Table.
To use a specific table instead, assign it before
Execute:
Script.Table := MyExistingTable; // not active: Script opens it after Execute
Script.Execute;
// MyExistingTable is now open with the result rows
If the table is already active when assigned, Execute
closes it, re-associates it with the new temp table, and reopens it.
TMMScript never frees a table provided by the caller.
Script.Cancel; // safe to call from any thread
Execute checks the cancellation flag at four points:
between upload chunks, after upload, after execution, and between result
pages. When cancelled, Script.Success = False and
Script.Cancelled = True.
Cancelhas no effect on a blocking HTTP call already in progress.
Two independent limits control how much BLOB/CLOB data is transferred
per Execute call.
Script.BlobMaxSizeBytes := 1024 * 1024; // 1 MB per cell; 0 = auto-derive from server limits
Script.InlineBlobMaxBytes := 64 * 1024; // 64 KB OOB threshold; 0 = all inline
BlobMaxSizeBytes — per-cell truncation cap. Cells larger
than this value are truncated server-side before encoding. The cap
applied per column is available in Columns[I].TruncCap
after Execute.
InlineBlobMaxBytes — out-of-band threshold. Cells whose
raw size exceeds this value are stored as server-side temp files and
fetched individually, keeping each result page small. Both values are
auto-derived from server limits after a successful
TestConnection call and do not need to be set manually in
most cases.
Inspect Columns[I].ActualMaxBytes after
Execute to see the actual maximum raw size seen per column
before truncation.
When the server aborted the result fetch before all rows were
processed (PHP OOM or MySQL timeout), or when a later page failed to
load into the EDB table, ResultIncomplete is set to
True; the partial result already in the local EDB table is
still valid data.
ResultIncomplete is automatically reset to
False at the start of each Execute call, so
re-running a query on the same TMMScript instance never
inherits a stale flag from a previous failure.
// Declare in your form or class:
procedure TMyForm.ScriptProgress(ASender: TMMScript; APhase: TMMScriptPhase;
ACurrent, ATotal: Int64; const AStatus: string);
begin
if APhase = spUploading then
ProgressBar1.Position := Round(ACurrent / Max(1, ATotal) * 100)
else if APhase = spBuilding then
lblStatus.Caption := Format('Loading rows: %d / %d', [ACurrent, ATotal]);
end;
// Assignment:
Script.OnProgress := ScriptProgress;
APhase indicates which stage is active
(spUploading, spExecuting,
spFetching, spBuilding). For upload progress,
ACurrent is the cumulative bytes uploaded and
ATotal is the total script size (UTF-8 bytes). For result
building, ACurrent is rows loaded and ATotal
is the total row count from the server response.
TMMUpdater writes rows from a local EDB buffer to a
remote MySQL/MariaDB table.
The workflow is: set properties → call Prepare → fill
Table → call Execute.
var Updater: TMMUpdater;
Updater := TMMUpdater.Create(nil);
try
Updater.Connection := Conn;
Updater.TargetTable := 'products';
Updater.Mode := umInsert;
Updater.UpdateColumns := ['name', 'price', 'stock']; // empty = all columns
if not Updater.Prepare then
begin
ShowMessage('Prepare failed — check OnLog for details');
Exit;
end;
// fill Updater.Table and call Execute (see below)
finally
FreeAndNil(Updater); // destructor drops the local EDB buffer table
end;
Prepare fetches the target table schema from the server,
validates the column list and mode rules, creates a local EDB temporary
table, and opens Table. Returns False on
schema fetch failure or validation error.
Use Updater.Table as a regular TEDBTable in
append mode:
Updater.Table.Append;
Updater.Table.FieldByName('name').AsString := 'Widget A';
Updater.Table.FieldByName('price').AsFloat := 9.99;
Updater.Table.FieldByName('stock').AsInteger := 100;
Updater.Table.Post;
// repeat for more rows...
Do not close or free Updater.Table directly — it is
managed by TMMUpdater.
When source data already exists in a local EDB table, populate the
buffer with a single SQL statement. TempTableName exposes
the name of the EDB buffer table:
Qry.SessionName := Conn.Database.SessionName;
Qry.DatabaseName := Conn.Database.DatabaseName;
Qry.SQL.Text :=
'INSERT INTO "' + Updater.TempTableName + '" (name, price, stock) ' +
'SELECT name, price, stock FROM local_products WHERE active = TRUE';
Qry.ExecSQL;
Updater.Table and direct SQL access operate on the same
EDB temporary table and can be combined freely.
Updater.Execute;
if not Updater.Success then
ShowMessage('Batch failed: ' + Updater.LastError.Msg)
else
ShowMessage(Format('%d row(s) sent, %d affected.',
[Updater.RowsSent, Updater.RowsAffected]));
Execute serialises all rows in chunks (default 512 KB
each) and posts each to the server. Result state is set in
Success, LastError.Msg,
RowsAffected, RowsSent, and
ChunksSent after Execute returns.
| Mode | Server action | Where required |
|---|---|---|
umInsert |
INSERT INTO ... VALUES (...) |
No |
umUpdate |
UPDATE ... SET ... WHERE <pk> |
No |
umUpsert |
INSERT ... ON DUPLICATE KEY UPDATE |
No |
For umUpdate, Prepare force-includes PK
columns in the buffer even when absent from UpdateColumns,
and the PHP backend routes them to the per-row WHERE predicate. Non-PK
columns go to SET. You may list PK columns in UpdateColumns
for clarity, but it is not required:
Updater.Mode := umUpdate;
Updater.UpdateColumns := ['id', 'price', 'stock'];
// PHP detects 'id' as PK → WHERE id = ? per row
// 'price' and 'stock' are non-PK → SET price = ?, stock = ?
Use Where only for an additional static filter applied
to every row (e.g. 'region = :region' with
Params.ParamByName('region').AsString := 'EU'), or when the
target table has no PRIMARY KEY.
Call Clear to empty the local table without recreating
it:
Updater.Execute;
Updater.Clear; // EMPTY TABLE on the EDB buffer; Table stays open
// fill and execute again...
// Declare in your form or class:
procedure TMyForm.UpdaterProgress(ASender: TMMUpdater; AChunksDone: Integer;
ARowsDone, ARowsTotal: Int64);
begin
ProgressBar1.Position := Round(ARowsDone / Max(1, ARowsTotal) * 100);
end;
// Assignment:
Updater.OnProgress := UpdaterProgress;
Updater.SkipFKChecks := True; // disables FK checks on the server for this batch
The server restores FK checks after the batch regardless of success or failure.
Prefetch loads rows from the remote table into the local
EDB buffer before Execute. Use it when you need to read
existing server values, modify them locally, and write them back.
Updater.Connection := Conn;
Updater.TargetTable := 'orders';
Updater.Mode := umUpdate;
Updater.UpdateColumns := ['id', 'status', 'updated_at'];
Updater.OnLog := HandleLog;
if Updater.Prepare then
if Updater.Prefetch('status = ''pending'' AND region = ''EU''') then
begin
Updater.Table.First;
while not Updater.Table.EOF do
begin
Updater.Table.Edit;
Updater.Table.FieldByName('status').AsString := 'processed';
Updater.Table.FieldByName('updated_at').AsDateTime := Now;
Updater.Table.Post;
Updater.Table.Next;
end;
Updater.Execute;
end;
Prefetch is available only in umUpdate and
umUpsert modes. Pass an empty string to load all rows.
BLOB and CLOB columns — data integrity warning:
Prefetch always downloads BLOB and CLOB values in
full. Every column in the buffer is later written back to the
server by Execute. A BLOB column included in
UpdateColumns that was fetched but not explicitly modified
would still be sent back on Execute — which is harmless if
the value arrived intact, but risks data loss if the download was
interrupted or failed silently.
The correct practice when the table has BLOB or CLOB columns that do
not need to be updated is to exclude them from
UpdateColumns:
// 'image' and 'document' are not in UpdateColumns — they are never fetched
// and never overwritten, regardless of their size on the server.
Updater.UpdateColumns := ['id', 'status', 'updated_at'];
Columns excluded from UpdateColumns are absent from the
buffer, absent from Prefetch, and absent from
Execute. The server-side values are never touched.
| Property | Type | Description |
|---|---|---|
Success |
Boolean | True when all chunks were accepted |
LastError |
TAppError |
Structured error; Code = EC_NONE on success |
FailedRowPK |
TArray<TPair<string,string>> |
PK col=val pairs of the row that caused a server-side failure (PHP v175+); empty on transport failures |
RowsInserted |
Int64 | Rows newly inserted
(umInsert/umUpsert) |
RowsUpdated |
Int64 | Rows updated by ON DUPLICATE KEY
(umUpsert) |
RowsMatched |
Int64 | Rows matched by WHERE (umUpdate) |
RowsAffected |
Int64 | Convenience total:
RowsInserted + RowsUpdated + RowsMatched |
RowsSent |
Int64 | Rows read from the local EDB buffer and sent |
ChunksSent |
Integer | Number of HTTP POST requests made |
TAppErroris declared inUErrorCodes. Add it touseswhen comparingLastError.CodeagainstEC_*constants (e.g.EC_HTTP_TIMEOUT,EC_CLIENT_CANCELLED). The type itself is visible throughEDBMMClientwithout the extrausesentry.
Both TMMScript and TMMUpdater expose the
name of their local EDB temporary table via TempTableName.
Any TEDBQuery in the same EDB session and database can read
from or write to that table using standard EDB SQL. This is often more
efficient than iterating Table row by row in Delphi.
var Qry: TEDBQuery;
Qry := TEDBQuery.Create(nil);
try
Qry.SessionName := Conn.Database.SessionName;
Qry.DatabaseName := Conn.Database.DatabaseName;
// Qry.SQL.Text := ...
// Qry.ExecSQL or Qry.Open
finally
Qry.Free;
end;
EDB identifiers must be quoted with ". Embed
TempTableName directly in the SQL string:
Qry.SQL.Text := 'SELECT COUNT(*) FROM "' + Script.TempTableName + '"';
After TMMScript.Execute, the result rows live in the EDB
temp table. An INSERT ... SELECT copies them (or a
filtered/transformed subset) into a persistent local EDB table for later
processing. TempTableName is valid for the lifetime of the
TMMScript object, or longer when
KeepTempTable = True.
Script.SQL.Text := 'SELECT id, name, amount, status FROM orders';
Script.Execute;
if Script.Success and Script.HasResultSet then
begin
// Copy only the pending rows into a persistent work table
Qry.SQL.Text :=
'INSERT INTO "pending_orders" (id, name, amount) ' +
'SELECT id, name, amount FROM "' + Script.TempTableName + '" ' +
'WHERE status = ''pending''';
Qry.ExecSQL;
// Script.Table is still open and valid after the INSERT
end;
Instead of appending rows one by one via
Table.Append / Post, bulk-load the buffer with a single
INSERT ... SELECT from an existing local EDB table. This
applies equally to umInsert, umUpdate, and
umUpsert modes.
Updater.TargetTable := 'products';
Updater.Mode := umUpdate;
Updater.UpdateColumns := ['id', 'price', 'stock'];
Updater.OnLog := HandleLog;
if not Updater.Prepare then
begin
ShowMessage('Prepare failed — check OnLog for details');
Exit;
end;
// Bulk-load with a SQL transformation: apply a 10 % price increase
Qry.SQL.Text :=
'INSERT INTO "' + Updater.TempTableName + '" (id, price, stock) ' +
'SELECT id, ROUND(price * 1.1, 2), stock ' +
'FROM "local_products" WHERE active = TRUE';
Qry.ExecSQL;
Updater.Execute;
Table.Append / Post and SQL INSERT operate
on the same temp table and can be combined freely: use SQL for bulk
loads and Table.Append / Post for rows that need
field-level Delphi logic.
This pattern is useful when the source and destination are
different tables, or when the data requires aggregation
or a JOIN before being written. When reading from and writing
back to the same table, use Prefetch instead
– it is designed exactly for that case.
Example: aggregate daily sales by product and update the running
total in the products table.
TMMUpdater.Prefetch cannot do this because the source
(daily_sales) and destination (products) are
different tables.
// Step 1: aggregate sales data from MySQL into a local EDB temp table
Script.SQL.Text :=
'SELECT product_id, SUM(qty_sold) AS total_sold ' +
'FROM daily_sales ' +
'WHERE sale_date >= ''2026-01-01'' ' +
'GROUP BY product_id';
Script.Execute;
if not Script.Success then
begin
ShowMessage('Read failed: ' + Script.LastError.Msg);
Exit;
end;
// Step 2: prepare the updater for the destination table
// PHP detects 'id' as PK of products → WHERE per row (no Where needed)
Updater.TargetTable := 'products';
Updater.Mode := umUpdate;
Updater.UpdateColumns := ['id', 'total_sold'];
Updater.OnLog := HandleLog;
if not Updater.Prepare then
begin
ShowMessage('Prepare failed — check OnLog for details');
Exit;
end;
// Step 3: transfer from script result into updater buffer --
// column rename (product_id -> id) handled in SQL, no Delphi loop needed
Qry.SQL.Text :=
'INSERT INTO "' + Updater.TempTableName + '" (id, total_sold) ' +
'SELECT product_id, total_sold ' +
'FROM "' + Script.TempTableName + '"';
Qry.ExecSQL;
// Step 4: write aggregated totals back to MySQL
Updater.Execute;
Script.Execute;
if Script.Success and Script.HasResultSet then
begin
Qry.SQL.Text :=
'SELECT COUNT(*) FROM "' + Script.TempTableName + '" WHERE amount > 1000';
Qry.Open;
ShowMessage(Format('%d high-value rows', [Qry.Fields[0].AsInteger]));
Qry.Close;
// Script.Table is still open and unaffected by the query above
end;
Avoid
DELETEorUPDATEon aTMMScriptresult table whileScript.Tableis open and being iterated – this can displace the active cursor. Either complete the iteration first, or closeScript.Tablebefore modifying the temp table contents.
Use FetchBlob on a TMMScript instance to
retrieve a single BLOB or CLOB column value after Execute,
when the value may have been truncated by the budget.
var
F : TField;
ColIdx : Integer;
PKParams: TJSONArray;
begin
F := Script.Table.FieldByName('photo');
ColIdx := F.Index;
PKParams := Script.BuildPKParams(Script.Table, ColIdx, 'products');
try
if Script.IsPKComplete('products', PKParams) then
Script.FetchBlob('products',
Script.Columns[ColIdx].SourceName,
PKParams, F);
finally
PKParams.Free;
end;
end;
BuildPKParams — builds the pk_cols array
from the current row, using original MySQL column names
(Columns[I].SourceName) so that EDB disambiguation suffixes
do not reach the server WHERE clause.IsPKComplete — verifies that the PK array covers all PK
columns for the source table (requires PHP v121+; returns
True conservatively on older servers).FetchBlob — downloads the complete value. Destination
order: ADestFile (evaluated first) →
ADestField → auto-named file in
DefaultTransferFolder.TMMConnection provides two typed methods that query
INFORMATION_SCHEMA.COLUMNS without executing a script. Both
parse the server response into TMMColumnInfo
records and additionally derive EDBType and
IsBinary via the library’s type mapper.
// Single column — parses JSON into a TMMColumnInfo record
var
Info: TMMColumnInfo;
begin
if Conn.GetColumnInfo('orders', 'status', Info) then
ShowMessage(Info.ColumnType + ' / EDB: ' + Info.EDBType);
end;
// All columns — parses JSON into a TMMColumnInfoArray
var
Cols: TMMColumnInfoArray;
C : TMMColumnInfo;
begin
if Conn.GetTableColumns('orders', Cols) then
for C in Cols do
ShowMessage(C.ColumnName + ': ' + C.EDBType);
end;
TMMColumnInfoandTMMColumnInfoArrayare declared inEDBMMUtils. They are accessible viauses EDBMMClient(which includesEDBMMUtilsin its interface clause), so an explicituses EDBMMUtilsis not required unless your unit uses these types independently.
See TMMColumnInfo
for field descriptions.