Using Apache Ignite from an Effect Agent
Use the Effect SDK's Ignite adapter. Native Node database drivers cannot run in the Golem WebAssembly runtime.
Imports
import { Effect, Schema } from "effect";
import { SqlClient } from "effect/unstable/sql";
import { method } from "@golemcloud/effect-golem";
import { Ignite, IgniteClient } from "@golemcloud/effect-golem/ignite2";
IgniteClient implements Effect's SQL client API over the golem:rdbms/ignite2@1.5.0 host
interface. SQL statements and connection setup are Effects that fail with Effect SQL errors.
Prefer the Canonical SqlClient Service
IgniteClient.layer provides both the Ignite adapter service and Effect's canonical
SqlClient.SqlClient tag. Keep portable repository code on that generic tag and provide the Golem
adapter at the application boundary:
const IgniteLive = IgniteClient.layer({
connectionAddress: "ignite://127.0.0.1:10800",
transformResultNames: (name) => name.toLowerCase(),
});
const findNote = (noteId: number) =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
return yield* sql<{ id: number; body: string }>`
SELECT id, body FROM notes WHERE id = ${noteId}
`;
});
const runnable = findNote(1).pipe(Effect.provide(IgniteLive));
SqlSchema, SqlResolver, and Migrator from effect/unstable/sql use this same service for
schema decoding, batching, and migrations. Keep Ignite imports only for Ignite-specific rich
parameter values or dialect behavior. Ignite still rejects nested withTransaction calls because
it has no savepoints; generic repository code must not assume nested transaction support.
Direct IgniteClient.make remains supported for a simple implementation that constructs one client
and closes over it. Use .layer when generic services or repository layers receive the client
through Effect context.
Open and Reuse a Connection
Ignite uses thin-client URLs such as ignite://host:10800. Construct the client once in the
agent's init Effect and return it as state before methods constructs the handlers:
Effect.gen(function* () {
const sql = yield* IgniteClient.make({
connectionAddress: "ignite://127.0.0.1:10800",
});
return {
// Agent method implementations close over `sql`.
};
});
Do not call IgniteClient.make in every method. It opens a host connection when the agent
implementation is initialized; reusing the client also serializes access correctly. The Ignite
host API has no explicit close operation.
Add the Agent Method Contract
Add the method to the existing agent's methods map and give it a string success schema:
checkIgnite: method({
input: {},
success: Schema.String,
description: "Checks the Ignite connection",
}),
Then add a handler with the same property name to the object returned by implement. Keep all
existing method contracts and handlers unchanged unless the task explicitly asks otherwise.
The agent type used by the CLI comes from the definition's name, not the exported TypeScript
constant. If the requested invocation is CounterAgent("db-test"), the definition must contain
name: "CounterAgent", not name: "Counter". Keep the matching agent key in golem.yaml
aligned as well:
httpApi:
deployments:
local:
- domain: test-app.localhost:9006
agents:
CounterAgent: {}
Changing only the exported constant name does not change the registered agent type.
Read the First Row and Column
Use a statement's .values Effect when column names do not matter. It returns positional rows as
ReadonlyArray<ReadonlyArray<unknown>>:
checkIgnite: () =>
Effect.gen(function* () {
const rows = yield* sql`SELECT 'ignite-ok'`.values;
const value = rows[0]?.[0];
if (typeof value !== "string") {
return yield* Effect.dieMessage(
"Expected Ignite row 0, column 0 to contain a string",
);
}
return value;
}),
This handler succeeds with a plain string; it does not expose a result wrapper in the agent
method contract. Do not return rows[0]?.[0] without checking it: an absent cell is undefined,
which does not satisfy Schema.String.
Normal statements return row records instead. Ignite commonly uppercases unquoted result column names. When record keys matter, either quote aliases or configure the client once:
Effect.gen(function* () {
const sql = yield* IgniteClient.make({
connectionAddress: igniteUrl,
transformResultNames: (name) => name.toLowerCase(),
});
return {
findNote: ({ noteId }: { noteId: number }) =>
sql`SELECT body FROM notes WHERE id = ${noteId}`,
};
});
Parameters and Statements
Use tagged-template interpolations for values. The adapter compiles each interpolation to an
Ignite ? placeholder and encodes ordinary JavaScript strings, numbers, booleans, bigints,
Uint8Array values, dates, null, and undefined:
const insertNote = Effect.gen(function* () {
yield* sql`
INSERT INTO notes (id, body)
VALUES (${noteId}, ${body})
`;
});
Use Ignite wrappers when the exact Ignite type matters:
const insertTypedNote = Effect.gen(function* () {
yield* sql`
INSERT INTO notes (id, body)
VALUES (${Ignite.int(1)}, ${Ignite.string("hello")})
`;
});
Do not write PostgreSQL-style $1 placeholders or manually construct host DbValue records.
Create Tables
Include WITH "CACHE_NAME=..." when creating a table through Ignite SQL:
const createNotesTable = Effect.gen(function* () {
yield* sql`
CREATE TABLE IF NOT EXISTS notes (
id INT PRIMARY KEY,
body VARCHAR
) WITH "CACHE_NAME=notes"
`;
});
If the table must support transactions, also configure ATOMICITY=TRANSACTIONAL in its Ignite
table options.
Transactions
Use withTransaction rather than a manually managed transaction object. It commits when the
Effect succeeds and invokes rollback when it fails:
const updateNote = Effect.gen(function* () {
yield* sql.withTransaction(
Effect.gen(function* () {
yield* sql`
INSERT INTO notes (id, body)
VALUES (${Ignite.int(1)}, ${"hello"})
`;
yield* sql`
UPDATE notes
SET body = ${"updated"}
WHERE id = ${Ignite.int(1)}
`;
}),
);
});
Ignite does not support nested transactions or savepoints through this adapter. Although the adapter invokes rollback on failure, some Ignite 2/H2 configurations do not undo DML reliably; verify rollback semantics for the deployed Ignite configuration.
Key Constraints
- Import the adapter from
@golemcloud/effect-golem/ignite2, not a native Node Ignite driver. - Prefer repository code that consumes
SqlClient.SqlClient; provide it withIgniteClient.layer. - Use
SqlSchema,SqlResolver, andMigratorfromeffect/unstable/sqlwith the same adapter layer, while avoiding nested transactions on Ignite. - Return Effects from agent handlers; do not replace them with
asyncfunctions. - Reuse one
IgniteClientper implemented agent instance. - Declare every added handler in the agent's
methodsmap with an Effect Schema. - Keep the
defineAgentname andgolem.yamlagent key equal to the type used for invocation. - Let Golem handle durable retries; do not add manual retry loops around SQL operations.
- Use
.valuesfor positional results or account for Ignite's default uppercase column names. - Ignite writes do not support SQL
RETURNINGthrough this adapter.