Creating and Managing a Non-Time-Series Table
Worked example: registering a non-time-series table with the AnyLog metadata layer
AnyLog is optimized for managing and querying time-series data. However, there are cases where users need to include non-time-series tables—such as asset information, user records, configuration data, reference data, or application-specific tables—in the AnyLog environment. By registering these tables with the AnyLog metadata layer, they become discoverable across the network. A query submitted to any AnyLog node can use the metadata to identify where the requested table is hosted, route the query to the appropriate node, and return the results to the requesting application. This allows non-time-series data to participate in the same distributed data environment as time-series data, without requiring the data to be centralized.
This document walks through the custom/manual path described in
Managing Relational (OLTP) Data in AnyLog: defining your own CREATE TABLE statement and
registering it with the AnyLog metadata layer, so the table remains discoverable and queryable from any node in the
network.
Use this workflow when the table needs constraints, keys, or indexes other than AnyLog’s default timestamp-based ones. If the default is sufficient, just push data to the node—AnyLog will generate the schema and policies automatically.
Example Environment
The example below uses a small three-node network—one master, one operator, and one query node—started and validated before the table is created.
# start nodes
make up ANYLOG_TYPE=master
make up ANYLOG_TYPE=operator
make up ANYLOG_TYPE=query
# validate they can communicate
AL d486935679d6-acme-query1 +> test network
Test Network
[****************************************************************]
Address Node Type Node Name Status
----------------|---------|-------------------------------|------|
172.27.0.2:32048|master |8f06476c3312-my_company-master1| + |
172.27.0.4:32348|query |d486935679d6-acme-query1 | + |
172.27.0.3:32148|operator |f0fb02d5d873-anylog-operator1 | + |
1. Create the Table
The table structure is defined by the user using a standard SQL CREATE TABLE statement. This statement isn’t run
against the database directly—it’s passed to AnyLog in the next step so the corresponding metadata policy can be
generated. The table is ultimately created on the AnyLog node that will host the data.
Example:
-- sample_file.sql
CREATE TABLE IF NOT EXISTS users(
username char(5),
password char(7),
role char(5),
email char(4),
associds char(4),
CONSTRAINT users_username_unique UNIQUE (username)
);
2. Register the Table with the Metadata Layer
The table definition needs to be represented by a policy in the AnyLog metadata layer. The policy describes the table name, database, and schema, allowing AnyLog to discover it across the network.
If you want data to be inserted and managed through AnyLog (as shown in Step 3), your CREATE TABLE statement
needs to include AnyLog’s system columns before you register it:
| Column | Definition | Purpose |
|---|---|---|
row_id |
SERIAL PRIMARY KEY |
unique row identifier |
insert_timestamp |
TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP |
when the row was inserted |
tsd_name |
CHAR(3) |
replication/HA-DR bookkeeping |
tsd_id |
INT |
replication/HA-DR bookkeeping |
along with two supporting indexes:
CREATE INDEX <table>_tsd_index ON <table>(tsd_name, tsd_id);
CREATE INDEX <table>_insert_timestamp_index ON <table>(insert_timestamp);
Use
CURRENT_TIMESTAMPrather than PostgreSQL’sNOW()for the default—SQLite (one of AnyLog’s supported backends) doesn’t supportNOW(). If the table will only ever be managed directly through the underlying database rather than through AnyLog, these columns and indexes aren’t required.
Example—extending Step 1’s sample_file.sql with the required columns and indexes:
CREATE TABLE IF NOT EXISTS users (
row_id SERIAL PRIMARY KEY,
insert_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
tsd_name CHAR(3),
tsd_id INT,
username char(5),
password char(7),
role char(5),
email char(4),
associds char(4),
CONSTRAINT users_username_unique UNIQUE (username)
);
CREATE INDEX users_tsd_index ON users(tsd_name, tsd_id);
CREATE INDEX users_insert_timestamp_index ON users(insert_timestamp);
Before registering, confirm there’s no existing table or cluster policy with the same database/table name—if either already exists, it can’t be reused:
blockchain get table where name=users and dbms=mydb
blockchain get cluster where table[name] = users and table[dbms] = mydb
Wrap the CREATE statement in a table policy and publish it to the blockchain:
<new_policy = {
"table": {
"dbms": "mydb",
"name": "users",
"create": "CREATE TABLE IF NOT EXISTS users (row_id SERIAL PRIMARY KEY, insert_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, tsd_name CHAR(3), tsd_id INT, username char(5), password char(7), role char(5), email char(4), associds char(4), CONSTRAINT users_username_unique UNIQUE (username));CREATE INDEX users_tsd_index ON users(tsd_name, tsd_id);CREATE INDEX users_insert_timestamp_index ON users(insert_timestamp);"
}
}>
blockchain insert where policy=!new_policy and local=true and master=!ledger_conn
Confirm the policy was registered:
AL f0fb02d5d873-anylog-operator1 +> blockchain get table where dbms=mydb
[{"table" : {"dbms" : "mydb",
"name" : "users",
"create" : "CREATE TABLE IF NOT EXISTS users (row_id SERIAL PRIMARY KEY, insert_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, tsd_name CHAR(3), tsd_id INT, username char(5), password char(7), role char(5), email char(4), associds char(4), CONSTRAINT users_username_unique UNIQUE (username));CREATE INDEX users_tsd_index ON users(tsd_name, tsd_id);CREATE INDEX users_insert_timestamp_index ON users(insert_timestamp);",
"id" : "c5e55bfc1cf20f4312cd27083ee3ae08",
"date" : "2026-08-11T18:51:17.392505Z",
"ledger" : "global"}}]
Before running this against a table that’s already deployed: if a table policy already exists for this
database/table name with a different create value, publishing will conflict and downstream inserts will fail with
“no such table” until the mismatch is resolved. Also make sure your SQL uses syntax valid across every SQL backend
you’re deploying to (AnyLog supports SQLite and PostgreSQL)—dialect-specific syntax with no equivalent on the other
engine (e.g. PostgreSQL’s SERIAL) isn’t caught automatically.
Note: There’s a limitation in the manual
CREATEabove—it hardcodes column types and constraints before seeing any data, whereas the standard AnyLog-generated table policy defines thecreatestatement based on the data once it arrives. Use the manual path only when you need constraints or indexes beyond AnyLog’s defaults.
3. Add or Update Data
Data is initially added by pushing a JSON stream containing the data to the table. The first JSON data pushed to the node triggers AnyLog to:
- Create the table and its schema in the local database assigned to the table.
- Register the node in the AnyLog metadata layer as a node that hosts data for this table.
- Insert the JSON data into the local table.
Therefore, pushing JSON data to the table at least once initializes the local table and makes its location discoverable through the AnyLog metadata layer.
After the table has been initialized, data can be added or updated in either of two ways:
- Through AnyLog: JSON data can continue to be pushed to the node hosting the table and inserted into the table.
- Directly by an application: An application can connect directly to the local database and modify the table
using standard SQL operations such as
INSERT,UPDATE, andDELETE.
Because the node hosting the table has already been registered with the AnyLog metadata layer, applications can subsequently manage the data directly in the local database without affecting the table’s discoverability across the AnyLog network.
Example:
curl.exe -X PUT "http://172.23.160.85:32149/" \
-H "type: json" \
-H "dbms: mydb" \
-H "table: users" \
-H "mode: streaming" \
-H "Content-Type: application/json" \
-d '[{"username":"oshad","password":"1234567","role":"admin","email":"osha","associds":"uid1"}]'
curl.exe -X PUT "http://172.23.160.85:32149/" \
-H "type: json" \
-H "dbms: mydb" \
-H "table: users" \
-H "mode: streaming" \
-H "Content-Type: application/json" \
-d '[{"username":"nwarn","password":"7543216","role":"user","email":"nsha","associds":"uid12"}]'
Constraint Enforcement
Because this sample workflow defines the schema with constraints like the UNIQUE (username) constraint from Step 1
are enforced at the database layer on every insert—including inserts pushed through AnyLog. If a row violates the
constraint, AnyLog does not silently drop, duplicate, or retry it; the row is rejected and the failure is recorded
in the node’s error log.
For example, resubmitting a username that already exists:
curl.exe -X PUT "http://172.23.160.85:32149/" \
-H "type: json" \
-H "dbms: mydb" \
-H "table: users" \
-H "mode: streaming" \
-H "Content-Type: application/json" \
-d '[{"username":"oshad","password":"1234567","role":"user","email":"osha","associds":"uid1"}]'
Check the error log on the operator that processed the insert:
AL 38797aba3631-anylog-operator1 +> get error log
ID Count Thread Time Type Text
---|-----|----------|------------------------|-----|----------------------------------------------------------------------------------------------------|
822| 1|rest_0 |Tue Aug 11 21:05:48 2026|Error|Error executing SQL: INSERT INTO par_users_2026_08_00_d14_insert_timestamp (row_id, insert_timestamp|
| | | | |, tsd_name, tsd_id, username, password, role, email, associds) VALUES (NULL, '2026-08-11T21:05:48.91|
| | | | |4348Z', 0, 7, 'oshad', '1234567', 'user', 'osha', 'uid1'); UNIQUE constraint failed: par_users_2026_|
| | | | |08_00_d14_insert_timestamp.username |
823| 1|rest_0 |Tue Aug 11 21:05:48 2026|Error|Failed to INSERT streaming data to local table with immediate flag: mydb.users |
825| 1|operator_0|Tue Aug 11 21:06:04 2026|Error|Error executing SQL: INSERT INTO par_users_2026_08_00_d14_insert_timestamp (row_id, insert_timestamp|
| | | | |, tsd_name, tsd_id, username, password, role, email, associds) VALUES (NULL, '2026-08-11T21:06:04.01|
| | | | |5412Z', 45, 8, 'oshad', '1234567', 'user', 'osha', 'uid1'); UNIQUE constraint failed: par_users_2026|
| | | | |_08_00_d14_insert_timestamp.username |
828| 1|operator_1|Tue Aug 11 21:06:09 2026|Error|Error executing SQL: INSERT INTO par_users_2026_08_00_d14_insert_timestamp (row_id, insert_timestamp|
| | | | |, tsd_name, tsd_id, username, password, role, email, associds) VALUES (NULL, '2026-08-11T21:06:04.01|
| | | | |5412Z', 45, 8, 'oshad', '1234567', 'user', 'osha', 'uid1'); UNIQUE constraint failed: par_users_2026|
| | | | |_08_00_d14_insert_timestamp.username |
829| 1|operator_1|Tue Aug 11 21:06:09 2026|Error|Error executing SQL from file: /app/AnyLog-Network/data/watch/mydb.users.2026_08_00_d14_insert_times|
| | | | |tamp.0.00ac17473055b70d6f49cda1f974b262.0.45.8.260811210603.1786482364.insert.sql |
830| 1|operator_1|Tue Aug 11 21:06:09 2026|Error|Failed to process SQL from file: /app/AnyLog-Network/data/watch/mydb.users.2026_08_00_d14_insert_tim|
| | | | |estamp.0.00ac17473055b70d6f49cda1f974b262.0.45.8.260811210603.1786482364.insert.sql |
The rejection shows up at every stage the row passes through: the initial REST-facing failure (“Failed to INSERT streaming data…”) on
rest_0, and the sameUNIQUE constraint failederror again onoperator_0andoperator_1as the cluster’s watch-folder mechanism processes the insert file. Useget error logto confirm a constraint is actually being enforced, and to diagnose rejected inserts in general.
4. Query the Table from Any Node
Applications do not need to know which physical node hosts the table. A query can be submitted to any AnyLog node in the network. The metadata layer identifies the node or nodes hosting the requested table and directs the query to the appropriate location. The query is executed where the data resides, and the results are returned to the requesting node. This provides a single logical view of the data while allowing the table and its data to remain distributed.
Example:
AL a4ee723b4064-acme-query1 +> run client () sql mydb format=table select username, password, role, email, associds from users
[4]
username password role email associds
-------- -------- ----- ----- --------
oshad 1234567 admin osha uid1
nwarn 7543216 user nsha uid12
{"Statistics":[{"Count": 2,
"Time":"00:00:00",
"Nodes": 1}]}