Client Library Concepts¶
Read this page once and the references, quickstart, and use cases will make sense. It covers the object model, the three protocols, how results reach you, and the type system.
One client, named clusters¶
Your program creates one Client. The client owns a background pool of I/O threads and manages one
or more clusters. A cluster is a named server target, you add it once, then refer to it by
name in every call.
Each cluster carries up to three configurations, one per protocol, and you only fill in the ones your program uses:
- a Relay (publish) config: to publish objects and register commands,
- a Center (subscribe) config: to subscribe to tables,
- a JDBC (SQL) config: to run queries and inserts.
Each configuration holds one or more shards: a shard is one server. A single cluster can span several servers per protocol; see Configuration → Sharding.
The three protocols¶
The protocols are independent connections with different shapes. A useful way to hold it: JDBC asks a question, Center watches a table, Relay publishes into the fabric.
| JDBC | Center | Relay | |
|---|---|---|---|
| Model | Request / response | Subscribe, then live push | Publish (fire-and-forget) |
| You call | sql, sql_insert, sql_replace |
subscribe |
publish, register_command |
| You get back | a result table | a snapshot, then on_update callbacks |
delivery is asynchronous; failures replay from an outbox |
| Writes data | yes (SQL / bulk) | no, read/subscribe only | yes (objects) |
Results reach you two ways: return values and callbacks¶
JDBC is synchronous by default. You call sql(...) and the rows come back as the return value; the
call blocks until the server answers (or sql_async takes a callback instead).
Center and Relay are event-driven. You attach a listener and implement the callbacks you care
about. When a subscribed row changes, the client invokes on_update; when a command arrives,
on_command; and so on. The callbacks that exist are on_connect, on_disconnect, on_logged_in,
on_message_sent, on_message_received, on_command, on_subscribe, on_update, and on_log.
Callbacks run on the client's own I/O threads, three rules
The library calls your listener from its internal threads, not your main thread. This has hard consequences you must respect:
- Copy the data you keep. A row-change update hands you views into the library's internal buffers that are valid only for the duration of the callback. Copy anything you store or pass to another thread; never hold the update object itself.
- Keep the callback fast; run blocking work on your own thread. The callback runs on the
client's I/O (delivery) thread, so blocking it stalls update delivery. A synchronous call on a
different connection (e.g: a JDBC
sql()from a Center callback) returns, but a same-connection synchronous call from its own callback can block. Queue slow work, and any same-connection call, to your own thread. - Guard your own shared state. Two updates can run on different threads; protect any structure your callback mutates.
Subscriptions: snapshot then deltas¶
When you subscribe to a table, the Center sends the current matching rows first, then a continuous
stream of changes. Each change arrives in on_update as a row-change update carrying:
- an action:
1= add,2= update,3= delete, - an
id: the row's stable numeric identity, present on every update (an add and its later updates and delete all carry the sameid), - a table name, and the changed data as parallel field-name / value lists.
There is also an object_id field, but it is the key you set when publishing an object to the
Relay, it is empty for rows created by SQL, so do not use it to identify subscription updates.
An update carries only the columns that changed, not the whole row. Your client must merge each
update onto the row it already holds, keyed by id: if you replace the whole row you blank the
untouched fields.
Merge on id, not object_id
Use the numeric id as the merge key. object_id is the Relay-publish key and is empty on
subscription updates for SQL-created rows: keying on it collapses every row under "" and loses
data. This was confirmed against a live instance: an added row and its later partial update both
carry the same id, while object_id is blank.
Knowing when the initial load is done. on_subscribe fires when the subscription reaches the
server, before the initial rows, which then arrive as on_update with action 1 (add). There is
no callback that marks the snapshot complete. If you need to block until the subscription is
established, pass sync=True to subscribe; otherwise treat the arrival of the initial add rows as
the snapshot itself.
You can filter and project on the server in C++: subscribe with a CenterSubscription to push a
WHERE filter and a column list to the Center, so it only sends the rows and columns you asked for.
Python has no equivalent: its subscribe takes table names only, so filter in your callback or
use the C++ client when you need the Center to do the filtering.
The type system¶
Both languages use the same type set, with the same wire values, so a value written by a C++ client reads back identically in Python. The common column types:
| Type | Type | Type |
|---|---|---|
| Boolean | Integer | String |
| Byte | Long | UUID |
| Short | Float | BigDecimal |
| Character | Double | BigInteger |
Binary data is carried as a Bytes/Binary column. A few types (UUID, BigDecimal, BigInteger) cannot
be told apart from their C++ representation alone, so the C++ client asks you to declare them
explicitly when you build a table for bulk insert, see C++ client → Bulk insert.
Temporal values are epoch integers, with no time zone
The type system does include date/time types (Date, DayTime, DateMillis, DateNanos), but a
temporal value travels as a raw epoch integer (seconds / milliseconds / nanoseconds) with no
time-zone information: the client applies no zone conversion, so agree the zone out of band
(UTC is the safe convention). Note also that the pure-Python pyami.pyjdbc layer cannot decode
these temporal types into a DataFrame; if you query over pyjdbc, carry an instant as a Long
epoch or a String you control.
What the clients do not do¶
- No transactions. The DB-API driver's
commitandrollbackare no-ops; there is no multi- statement transaction. - One JDBC request at a time per cluster. The SQL socket does not queue concurrent requests; serialize them, or use separate clusters.
- No subscribe over JDBC, no SQL over Center. Each protocol does one job; compose them.
Verified live via the Python client (build 32437)
The subscription and callback behavior here was reproduced against a live instance, including
the merge-by-id correction. The C++ examples share the same model but were not separately run.