KalamDB Server Functions, Typed Contracts & Topic Triggers
September 5, 2026 · View on GitHub
0.7 program: Functions V1 is one of three essential tracks for KalamDB 0.7. Implement it alongside centralized serialization and scalar secondary indexes. Combined sequence and release gate: 2026-09-01-kalamdb-0.7.md.
Development plan: See functions-v1-implementation.md for the frozen V1 scope, dependency-ordered tasks, verification checkpoints, and corrected CLI deployment sequence.
Status: Proposed
Version: 0.2
Primary design principle: SQL is the source of truth for the complete backend contract.
1. Vision
Extend KalamDB from:
SQL Database
+ Realtime
+ Durable Topics
into:
SQL Database
+ Realtime
+ Durable Topics
+ Server-side application logic
+ Generated typed client SDK
without introducing a separate REST endpoint model or a second object/type system.
Applications use SQL as the common contract:
SELECT → query
INSERT → direct write
UPDATE → direct write
DELETE → direct write
SUBSCRIBE → realtime
PUBLISH → asynchronous event
CALL → application/business operation
A procedure can be invoked directly:
Client
│
│ CALL chat.create_message(...)
▼
Procedure
or from an event source:
Topic
│
▼
Trigger
│
▼
Procedure
or later from the scheduler:
Schedule
│
▼
Procedure
The same procedure implementation is reused by every invocation source.
2. Core terminology
Use Kalam Functions as the product/feature name.
Procedure
A transactional business operation invoked with CALL.
CALL chat.create_message(...);
A procedure may:
- read tables;
- insert/update/delete;
- publish topics;
- call other procedures;
- return typed values;
- execute under the authenticated
ExecutionContext.
Function
Expression-style/read-only computation used inside SQL expressions.
SELECT COSINE_DISTANCE(...);
SELECT SNOWFLAKE_ID();
Existing DataFusion UDFs remain unchanged.
Trigger
Connects an event source to an existing procedure.
TOPIC → PROCEDURE
Future sources may include:
TABLE CHANGE → PROCEDURE
SCHEDULE → PROCEDURE
HTTP → PROCEDURE
A trigger does not define a new function runtime.
3. SQL and PostgreSQL compatibility
KalamDB should deliberately follow PostgreSQL syntax and semantics wherever the feature maps naturally to PostgreSQL.
This includes:
CREATE SCHEMA
schema.object qualification
SET search_path
CREATE TYPE ... AS (...)
CREATE TYPE ... AS ENUM (...)
ALTER TYPE ... ADD/DROP/ALTER ATTRIBUTE
implicit table row/composite types
composite values as columns
SETOF
CALL
GRANT EXECUTE ON PROCEDURE
RESTRICT / CASCADE dependency semantics
KalamDB-specific syntax should be introduced only where PostgreSQL has no direct equivalent or KalamDB needs an explicit product feature.
Existing KalamDB namespace aliases may remain supported:
USE chat;
USE NAMESPACE chat;
SET NAMESPACE chat;
but PostgreSQL-compatible syntax is preferred in public SQL examples:
CREATE SCHEMA chat;
SET search_path TO chat;
Canonical catalog identifiers are always schema-qualified.
4. PostgreSQL-style table row types
PostgreSQL automatically creates a composite row type for every table. KalamDB should follow the same semantic model.
Given:
CREATE TABLE chat.users (
id BIGINT PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL
);
KalamDB exposes the implicit type:
chat.users
in a type context.
Therefore this is valid:
CREATE PROCEDURE chat.get_user(
user_id BIGINT NOT NULL
)
RETURNS chat.users;
The same type may be used as a physical nested column:
CREATE TABLE audit.entries (
id BIGINT PRIMARY KEY,
actor chat.users
);
audit.entries.actor stores a value snapshot. It is not a pointer to the original row and is not equivalent to a foreign key.
If the original row changes later, the embedded actor value does not change automatically.
If reference semantics are wanted, use a normal key/reference column instead:
CREATE TABLE audit.entries (
id BIGINT PRIMARY KEY,
actor_id BIGINT REFERENCES chat.users(id)
);
5. Optional singular row aliases
KalamDB may keep the existing explicit singular alias because it gives better generated SDK names without duplicating table fields.
Inline form:
CREATE TABLE chat.users (
id BIGINT PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL
) ROW TYPE chat.user;
Standalone form:
CREATE TYPE chat.user FROM TABLE chat.users;
These are KalamDB syntax extensions, but their semantics are simple:
chat.user
↓ live alias
chat.users implicit table row type
chat.user does not copy the field list and cannot drift from the table.
Both references are valid where a type is expected:
CREATE PROCEDURE chat.get_user(user_id BIGINT)
RETURNS chat.user;
CREATE TABLE audit.entries (
actor chat.user
);
Rules:
- every table always has its PostgreSQL-style implicit row type;
ROW TYPE/FROM TABLEcreates an optional second logical name;- the alias tracks
ALTER TABLEautomatically; - no English inflection is performed automatically;
- one explicit row alias per table in V1;
- the alias and source table remain in the same schema in V1;
- retargeting an existing alias to another table is rejected;
- embedded row values inherit field names/types/nullability, not table-only behavior such as PK uniqueness, indexes, FKs, or RLS;
- storage-only metadata is not injected into the logical row type unless it is already part of the SQL-visible query schema.
6. PostgreSQL-style reusable composite types
For a reusable object that is not tied to one table, use PostgreSQL-style CREATE TYPE ... AS (...).
CREATE TYPE app.address AS (
city TEXT,
country TEXT,
postal_code TEXT
);
This type is reusable everywhere:
CREATE TABLE app.customers (
id BIGINT PRIMARY KEY,
address app.address
);
CREATE PROCEDURE app.get_address(
customer_id BIGINT
)
RETURNS app.address;
CREATE TYPE app.customer_summary AS (
id BIGINT,
name TEXT,
address app.address
);
CREATE TOPIC app.customer_changed
TYPE app.customer_summary;
The same SQL type definition drives:
table columns
procedure inputs
procedure outputs
topic payloads
nested objects
generated TypeScript
generated Dart
generated Rust
There is one schema source of truth.
7. Anonymous Structs
For one-off nested structures that do not deserve a reusable name, KalamDB may expose DataFusion-compatible STRUCT(...) syntax.
CREATE TABLE app.events (
id BIGINT,
client STRUCT(
ip TEXT,
device TEXT
)
);
Rule:
Use anonymous
STRUCT(...)for local one-off shapes. UseCREATE TYPE ... AS (...)for reusable domain objects.
Generated TypeScript may inline the anonymous object shape.
8. One logical type system, DataFusion/Arrow physical types
KalamDB must not introduce a separate function-object type system.
The mapping is:
SQL / Kalam contract
↓
ContractTypeRef
↓
DataFusion / Arrow physical type
Examples:
BOOLEAN → Arrow Boolean
BIGINT → Arrow Int64
TEXT → Arrow Utf8
app.address → Arrow Struct
chat.users → Arrow Struct
chat.user alias → same Arrow Struct as chat.users
T[] → Arrow List<T>
STRUCT(...) → Arrow Struct
Named types keep their logical TypeId and schema-qualified name in KalamDB's catalog even though DataFusion receives a resolved Arrow DataType::Struct.
DataFusion should not be responsible for KalamDB's named-type catalog.
The compiler/catalog resolves:
app.address
↓
Struct<
city: Utf8,
country: Utf8,
postal_code: Utf8
>
before planning/execution/storage code that needs the physical type.
9. In-memory value representation
Use DataFusion/Arrow values as the common in-memory representation.
Conceptually:
single scalar ScalarValue
single composite/row ScalarValue::Struct
column of composites StructArray
array/list ListArray
query batch RecordBatch
KalamDB should not normalize these values through serde_json::Value internally.
For example, avoid:
ScalarValue::Struct
↓
serde_json::Value
↓
JSON text
↓
parse again
↓
TypeScript object
Prefer:
ScalarValue::Struct
↓
typed host/binary conversion
↓
TypeScript object
Nested in-process procedure calls should avoid serialization entirely where possible.
10. Shared serialization architecture
KalamDB already has a common serialization boundary in kalamdb-commons.
Use it instead of introducing function-specific serialization.
Current architecture:
KSerializable
├── generic entity default → FlexBuffers
└── typed row/scalar codec → FlatBuffers
EntityEnvelope
├── codec_kind
├── schema_version
└── payload
The existing row codec already operates on DataFusion ScalarValue and uses FlatBuffers for row/scalar persistence.
Kalam Functions should extend and reuse this exact path.
Required change:
ScalarValue::Struct,List, and nested Struct/List combinations must become first-class typed FlatBuffers variants. They must never fall through to the generic string fallback.
11. Fast Struct serialization
For schema-known composites and table rows, the type definition already provides field names and field order.
Do not repeat field names inside every stored nested object if they can be resolved from the type/schema.
Preferred persisted representation:
TypeId / schema hash
+
ordered values
rather than:
{
"first_name": ...,
"last_name": ...,
"user_name": ...
}
for every value.
Conceptually:
app.user schema
0 first_name TEXT
1 last_name TEXT
2 user_name TEXT
stored value
["Jamal", "Saad", "jamal"]
The FlatBuffers payload carries typed values; the catalog/schema identifies the field semantics.
This reduces:
repeated field-name bytes
hash-map allocations
JSON object allocations
JSON parsing
string comparisons during decode
and keeps the representation aligned with Arrow's schema + arrays model.
12. RocksDB serialization
Nested composite/row columns stored in RocksDB must use the same KSerializable / typed FlatBuffers path as existing table rows.
Preferred flow:
SQL INSERT
↓
resolved Arrow schema
↓
Row with ScalarValue::Struct
↓
FlatBuffers row/scalar encoder
↓
EntityEnvelope
↓
RocksDB bytes
Read:
RocksDB bytes
↓
EntityEnvelope validation
↓
FlatBuffers row/scalar decoder
↓
ScalarValue::Struct
↓
DataFusion / function runtime
Do not insert a JSON representation between RocksDB and DataFusion.
KSerializable remains the storage boundary API.
13. Parquet / cold storage
For flushed cold data, nested types should remain Arrow-native:
Struct
List
Struct<List<...>>
Parquet supports nested Arrow-compatible structures, so KalamDB should write nested columns directly rather than storing a composite as serialized JSON text.
This preserves:
columnar representation
nested field projection
type validation
better compression
query optimization opportunities
14. Schema evolution for composites
KalamDB should follow PostgreSQL-style DDL where possible:
ALTER TYPE app.user
ADD ATTRIBUTE user_name TEXT;
ALTER TYPE app.user
DROP ATTRIBUTE user_name;
ALTER TYPE app.user
ALTER ATTRIBUTE user_name TYPE VARCHAR;
ALTER TYPE app.user
RENAME ATTRIBUTE user_name TO username;
For a table row type, normal table evolution is authoritative:
ALTER TABLE chat.users
ADD COLUMN username TEXT;
The implicit chat.users row type and a bound chat.user alias update automatically.
15. No eager historical rewrite for compatible additions
Suppose an old stored value is:
Struct<
first_name,
last_name
>
and the logical type evolves to:
Struct<
first_name,
last_name,
user_name
>
KalamDB should not rewrite every RocksDB/Parquet row merely because a nullable field was added.
On read, schema projection materializes:
user_name = NULL
for historical values that predate the field.
If a field has an explicit safe default and KalamDB's migration semantics define default-on-read, that may be applied explicitly.
Breaking changes still require the normal migration compatibility checks.
16. Constraint semantics for embedded row types
A table row type carries its logical value shape, not all behavior of the source table.
Given:
CREATE TABLE chat.users (
id BIGINT PRIMARY KEY,
email TEXT UNIQUE,
display_name TEXT NOT NULL
);
an embedded chat.users value carries:
id
email
display_name
field types
contract nullability
but does not turn the containing table into another copy of the source constraints.
Do not inherit as embedded constraints:
PRIMARY KEY
UNIQUE
indexes
foreign keys
RLS policies
This mirrors the idea that a composite value is data, not another relational table.
17. Generated code and schema-first workflow
SQL remains authoritative.
Example:
CREATE TYPE app.user AS (
first_name TEXT NOT NULL,
last_name TEXT NOT NULL
);
Generated TypeScript:
export interface AppUser {
firstName: string;
lastName: string;
}
Then SQL changes to:
CREATE TYPE app.user AS (
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
user_name TEXT NOT NULL
);
kalam dev should automatically:
schema.sql changed
↓
compile ContractSnapshot
↓
resolve new Arrow Struct
↓
regenerate SDK/server contracts
↓
rebuild functions
An implementation that still returns only two fields fails compilation/validation until updated.
The developer should not manually synchronize SQL and language models.
18. Generated TypeScript example
CREATE TYPE app.address AS (
city TEXT,
country TEXT
);
CREATE TABLE app.users (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL,
address app.address
) ROW TYPE app.user;
Generated:
export interface AppAddress {
city: string | null;
country: string | null;
}
export interface AppUser {
id: bigint;
name: string;
address: AppAddress | null;
}
The alias reuses the table row shape; it is not generated as a separate drifting model.
19. Procedure inputs and outputs
Procedure parameters and returns use the same SQL type system.
CREATE TYPE chat.create_message_request AS (
group_id TEXT NOT NULL,
body TEXT NOT NULL,
reply_to BIGINT
);
CREATE PROCEDURE chat.create_message(
request chat.create_message_request
)
RETURNS chat.message;
The implementation receives generated typed input and returns the generated result type.
For simple procedures, ordinary scalar parameters remain preferable.
20. Procedure return categories
Support:
VOID
scalar
enum
table row / row alias
SETOF table row
SETOF named composite
SETOF enum
named composite
JSONB
Use JSONB only when there is genuinely no stable schema.
Typed objects should not be declared JSONB merely because a browser consumes them.
21. Runtime validation
Generated language typing is not sufficient on its own.
At runtime boundaries KalamDB validates:
argument count
argument SQL type
Struct field type
nullability
NONEMPTY constraints
return value shape
topic payload type
Validation uses the resolved contract/Arrow type metadata.
It must not require serializing the value into JSON first.
22. Transport behavior
The logical/internal value remains typed until an edge protocol requires formatting.
For generated SDK/runtime paths, prefer:
typed binary/direct conversion
For a generic JSON HTTP client, a composite may be formatted as JSON at the edge:
{
"id": "10",
"name": "Jamal"
}
That JSON representation is not the internal object model.
For PGWire:
scalar → normal PostgreSQL scalar column
row → normal columns where result shape is flattened
SETOF → normal rows
nested Struct → PostgreSQL-compatible composite representation where supported
Native PostgreSQL composite OID/protocol behavior should be approached incrementally, but KalamDB must not force RocksDB/runtime storage through JSON just because an edge protocol initially needs a compatibility encoding.
23. Topics
Typed topics use the same type system.
CREATE TYPE chat.message_created_event AS (
message_id BIGINT,
group_id TEXT,
sender_id TEXT
);
CREATE TOPIC chat.message_created
TYPE chat.message_created_event;
A typed topic payload is encoded using the same shared type resolver and binary codec as procedure values.
Untyped topics remain supported and behave as JSONB payloads.
24. Topic triggers
CREATE TRIGGER chat.process_message
ON TOPIC chat.message_created
EXECUTE PROCEDURE chat.on_message_created(PAYLOAD);
PAYLOAD is the V1 business binding.
Topic metadata such as:
event id
partition
offset
attempt
actor
is supplied through the trusted invocation context, not as client-controlled SQL parameters.
Each trigger owns an internal durable consumer group and therefore gets independent offset/retry state.
25. Topic delivery semantics
Use at-least-once delivery:
read topic
↓
invoke procedure
↓
transaction
↓
commit
↓
ACK
Failure:
rollback
↓
no ACK
↓
retry
Topic publication from a procedure participates in the procedure transaction.
26. External topic consumers remain first-class
Topic-triggered procedures do not replace the existing external consumer model.
KalamDB Topic
↓
consumer group
↓
external service / agent / worker
External consumers still use:
CONSUME
ACK
@kalamdb/consumer
Rust consumer API
Python consumer API
The same topic may have both an internal trigger consumer group and external application consumer groups.
27. Procedure project
A deployed function module is a complete project.
functions/
├── package.json
├── tsconfig.json
├── src/
│ ├── chat/
│ │ ├── create_message.ts
│ │ └── process_message.ts
│ └── common/
│ └── validation.ts
└── .kalam/
└── generated/
├── contracts.ts
└── registry.ts
Developers may use helpers, classes, modules, npm packages, tests, and normal project structure.
The SQL contract is not redefined in code.
28. No manual SQL-to-code mapping
For:
CREATE PROCEDURE chat.create_message(...)
Kalam uses the conventional implementation path:
functions/src/chat/create_message.ts
kalam generate scaffolds the file once when missing and never overwrites developer implementation code.
29. Generated server contract
import {
defineProcedure,
type ChatCreateMessage,
} from "../.kalam/generated/contracts";
export default defineProcedure<ChatCreateMessage>(
async (ctx, input) => {
// implementation
},
);
Changing SQL regenerates the contract and causes the language compiler to point out implementation drift.
30. No loopback HTTP inside functions
Do not implement:
V8/Wasm
↓
HTTP
↓
KalamDB
Use:
V8/Wasm
↓
Kalam Host API
↓
existing executor / transaction layer
ctx.db, ctx.functions, and ctx.topics are in-process host APIs.
31. Unified ExecutionContext
Use one root execution abstraction.
The existing KalamDB ExecutionContext should be extended instead of introducing a second heavyweight function context.
Conceptually:
ExecutionContext
├── identity / principal / actor
├── namespace / schema
├── request metadata
├── cancellation
├── transaction scope
├── function stack
└── lazy DataFusion context
DataFusion is a tool owned by the execution context, not the execution abstraction itself.
A procedure may finish without creating a DataFusion SessionContext.
32. Automatic transactions
A root CALL is transactional by default.
CALL app.function1()
↓
ExecutionContext
↓
first mutation
↓
lazy transaction
↓
nested work
↓
root success?
yes → COMMIT
no → ROLLBACK
Transactions begin lazily on first mutation.
A procedure inside explicit SQL BEGIN borrows that transaction and does not commit independently.
Nested procedure calls reuse the same root transaction.
33. Nested calls
Generated server SDK:
await ctx.functions.chat.fanout({
messageId: message.id,
});
Nested calls reuse:
ExecutionContext
TransactionScope
CancellationToken
principal / actor
requestId
module revision
and push one procedure ID onto the root function stack.
No HTTP call, new transaction, actor, or second root execution is created.
Typed parameters/results should be passed directly in memory where possible.
34. Nested failure semantics
A transactional failure in a nested procedure aborts the root transaction in V1, even when application code catches the error.
This follows PostgreSQL-like failed-transaction behavior.
The host maintains the cross-procedure call stack because JavaScript stack traces cannot represent host-mediated nested procedure boundaries.
35. Execution tracking
Track only currently active root executions.
DashMap<ExecutionId, Arc<ExecutionControl>>
Use compact numeric IDs and atomic enum state.
Do not persist start/end rows for every high-frequency function execution.
Expose a virtual in-memory view such as:
SELECT * FROM system.active_function_runs;
and use metrics for aggregate history.
36. CALL fast path
A simple procedure invocation should bypass DataFusion planning when possible:
SQL parse/classify
↓
resolve ProcedureCallPlan
↓
bind typed arguments
↓
invoke runtime
DataFusion is used only when the procedure performs query/expression work requiring it.
Procedure resolution should reuse KalamDB's existing SQL/plan cache infrastructure.
37. Runtime abstraction
Keep the execution/runtime contract language-neutral:
KalamModuleRuntime
load(module)
invoke(procedure, context, args)
cancel(execution_id)
shutdown()
TypeScript initially uses a sandboxed V8 adapter.
Rust/compiled runtimes may use Wasmtime later.
The public function contract must not depend on the language runtime implementation.
38. TypeScript runtime
TypeScript should be a real TypeScript project:
TypeScript
↓
tsc/esbuild
↓
bundled JavaScript
↓
V8 isolate
Support:
multiple files
modules
classes
async/await
pure JS/TS npm dependencies
tests
Do not provide unrestricted Node capabilities by default:
fs
net
child_process
process.env
native addons
Capabilities are explicit.
39. V8 typed ABI
The V8 host ABI should convert generated language objects to/from the shared Kalam/DataFusion value model without JSON stringify/parse.
Conceptually:
JS object
↓ generated signature-aware conversion
RoutineValue / ScalarValue::Struct
↓
procedure host
A reusable RoutineValue abstraction may exist, but it must be a thin wrapper over the shared DataFusion/Arrow type/value model rather than another JSON tree.
40. Generated artifacts
kalam generate compiles one canonical snapshot:
schema SQL
↓
Contract Compiler
↓
ContractSnapshot
├── schemas
├── tables
├── implicit row types
├── row aliases
├── named composite types
├── enums
├── topics
├── procedures
└── triggers
The same snapshot drives:
schema diff
Arrow type resolution
server contracts
TypeScript SDK
Dart SDK
Rust SDK
function build validation
deployment compatibility
Do not create separate parsers for each consumer.
41. Local development
kalam dev watches:
schema SQL
functions/src/**
package manifest / lockfile
generated SDK targets
On schema change:
parse/validate
↓
schema diff
↓
resolve types
↓
regenerate SDK/server contracts
↓
rebuild functions
↓
hot activate local revision
Adding/removing a field from a reused CREATE TYPE automatically updates every generated dependent type.
42. Deployment
kalam deploy should:
1. Compile local ContractSnapshot
2. Diff physical schema including nested Struct/type changes
3. Require committed migrations when appropriate
4. Generate contracts
5. Build function project
6. Validate exports, ABI, permissions, types and serde compatibility
7. Apply physical migrations
8. Upload immutable artifact
9. Atomically reconcile contract catalog + active revision in Meta-Raft
10. Load/validate required runtime nodes
11. Report success after health quorum
Repeated deployment of the same contract/artifact hashes is a no-op.
43. Catalog model
Types and routines are first-class system metadata.
Conceptual tables:
system.types
system.type_fields
system.type_bindings
system.routines
system.routine_parameters
system.routine_grants
system.triggers
system.function_modules
system.function_revisions
system.function_artifacts
system.types.kind should distinguish at least:
implicit_table_row
row_alias
composite
enum
A row alias records the source table/type ID instead of copying the table field list.
Catalog rows retain logical named types while the shared resolver derives DataFusion/Arrow physical types.
44. information_schema / PostgreSQL catalogs
Expose standards-compatible routine/type information through information_schema where useful.
Because KalamDB supports PGWire/JDBC tooling, also expose PostgreSQL-compatible catalog metadata wherever practical so clients can discover:
schemas
tables
columns
composite types
enums
procedures
procedure parameters
The catalog compatibility layer should reflect the same canonical contract rather than maintaining duplicate metadata.
45. Type evolution and dependencies
Schema diff/deploy must know the dependency graph:
Type
├── table columns
├── procedure args
├── procedure returns
├── topic payloads
├── trigger target signatures
└── other composite fields
Compatible additions can be applied without rebuilding historical rows.
Breaking changes require migration/compatibility validation.
Use PostgreSQL-like RESTRICT as the safe default for destructive DDL.
46. Serialization benchmarks
Typed binary serde is a performance feature and must be benchmarked rather than assumed.
Benchmark representative payloads:
scalars
small Struct
nested Struct
Struct + List
large list of Structs
table row alias
topic payload
procedure request/response
Measure:
encode latency
decode latency
allocations
payload bytes
CPU
throughput
Compare:
current typed scalar codec
new nested FlatBuffers codec
serde_json JSON path
FlexBuffers where applicable
The target is not a new serializer for functions; the target is extending the existing common codec efficiently.
47. Error behavior
Errors should follow existing KalamDB/PostgreSQL-style SQL errors and stable KalamDB error codes.
Examples:
undefined type
incompatible composite assignment
missing required Struct field
unknown Struct field
procedure return type mismatch
schema dependency violation
execution transaction aborted
PGWire maps the same failures to appropriate SQLSTATE plus DETAIL/HINT where practical.
Generated SDKs surface the same stable KalamDB error code.
48. Recommended create_message example
CREATE TABLE chat.messages (
id BIGINT PRIMARY KEY DEFAULT SNOWFLAKE_ID(),
group_id TEXT NOT NULL,
sender_id TEXT NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
) ROW TYPE chat.message;
CREATE TYPE chat.delivery_result AS (
user_id TEXT NOT NULL,
delivered BOOLEAN NOT NULL
);
CREATE TYPE chat.send_message_result AS (
message chat.message NOT NULL,
deliveries chat.delivery_result[] NOT NULL,
fanout_count INT NOT NULL
);
CREATE TYPE chat.message_created_event AS (
message_id BIGINT NOT NULL,
group_id TEXT NOT NULL,
sender_id TEXT NOT NULL
);
CREATE TOPIC chat.message_created
TYPE chat.message_created_event;
CREATE PROCEDURE chat.create_message(
group_id TEXT NOT NULL,
body TEXT NOT NULL
)
RETURNS chat.send_message_result
SECURITY INVOKER;
49. Architecture summary
SQL schema
│
▼
Contract Compiler
│
ContractSnapshot
│
┌────────────────┼────────────────┐
▼ ▼ ▼
Catalog Arrow Resolver Codegen
│ │ │
│ ▼ ├── TS
│ DataFusion types ├── Dart
│ │ └── Rust
│ ▼
│ ScalarValue / Arrays
│ │
│ ┌─────────┴──────────┐
│ ▼ ▼
│ In-memory Durable boundary
│ │ │
│ direct host KSerializable
│ values FlatBuffers/FlexBuffers
│ │
│ RocksDB / topics
│
▼
procedures/triggers
Cold table storage remains Arrow/Parquet-native.
50. Core type requirements
The type system SHALL follow these requirements:
PostgreSQL composite semantics are the baseline.
Every table has an implicit same-named row/composite type.
ROW TYPE / CREATE TYPE ... FROM TABLE is an optional KalamDB singular alias, not the underlying row-type mechanism.
CREATE TYPE ... AS (...) defines reusable composites using PostgreSQL-like syntax.
Composite and row types may be used as physical table columns.
Anonymous STRUCT(...) may be used for one-off nested shapes.
All composite/row/anonymous Struct values resolve to DataFusion/Arrow Struct types.
Arrays resolve to Arrow List types.
The catalog retains logical named type identity while DataFusion receives resolved physical types.
RocksDB persistence reuses KSerializable, EntityEnvelope, and the existing FlatBuffers row/scalar codec.
The scalar codec must be extended so supported Struct/List values never use string fallback.
Schema-known nested values should use ordinal encoding and schema/type identity rather than repeatedly persisting field names.
Typed values remain typed in memory and are not normalized through JSON.
Nested in-process procedure calls avoid serialization where possible.
JSON/JSONB remains an explicit dynamic SQL type and an edge compatibility encoding, not the internal default.
Parquet stores nested Arrow structures natively.
Compatible additive schema changes should not trigger eager rewrites of historical data.
51. Unified root execution
KalamDB SHALL use one root execution abstraction for direct CALL, PGWire invocation, topic triggers, and later scheduled procedures.
The existing ExecutionContext is extended rather than replaced.
A normal lightweight SQL query may continue using an ephemeral context without active-function registration.
52. Execution control and cancellation
Tracked roots receive lightweight ExecutionControl state:
execution id
procedure id
started time
state
operation
cancellation token
Use compact IDs/enums/atomics and an active-only registry.
Cancellation propagates to:
V8
Wasmtime
DataFusion
host DB calls
nested procedures
Once durable commit is in progress, cancellation must not falsely claim rollback.
53. Observability
Do not emit persistent start/end records for every invocation.
Use aggregate metrics:
function_invocations_total
function_errors_total
function_cancellations_total
function_duration_histogram
Detailed logs/traces are emitted for:
explicit ctx.log
errors
slow executions
sampled traces
54. Actor frameworks are implementation details
KalamDB is not an actor database and its public function model must not depend on an actor abstraction.
Long-lived infrastructure may use an actor library if useful, but high-frequency root executions should default to the simplest efficient Tokio/runtime model unless benchmarks prove otherwise.
Transactional function semantics are defined by:
ExecutionContext
TransactionScope
ExecutionControl
ActiveExecutionRegistry
not actor lifecycle rules.
55. Scheduler
Scheduling is another SQL-defined way to invoke an existing procedure.
Scheduler decides when
↓
CALL existing procedure
↓
normal ExecutionContext
↓
normal transaction/runtime
A scheduled procedure is not a new function type.
56. Schedule SQL
Keep the existing MySQL-familiar event syntax for KalamDB scheduling because it is concise and SQL-native:
CREATE EVENT reports.daily_summary
ON SCHEDULE CRON '0 9 * * *'
TIME ZONE 'Asia/Jerusalem'
DO CALL reports.generate_daily_summary(...);
Support:
AT one-time
EVERY fixed interval
CRON calendar schedule
Lifecycle:
ALTER EVENT reports.daily_summary ENABLE;
ALTER EVENT reports.daily_summary DISABLE;
DROP EVENT reports.daily_summary;
Scheduling remains a post-V1 implementation item unless explicitly promoted into the V1 plan.
57. Schedule reliability
Schedule definitions are durable catalog state.
In-memory timers are only an optimization.
Recommended policies:
retries
retry_backoff
overlap = skip | queue | allow
misfire = run_once | skip | catch_up
Use an index ordered by next_run_at; do not allocate one OS timer/actor per schedule.
In a cluster, one owner dispatches each logical occurrence using existing leadership/lease mechanisms.
58. Scheduler transactions
Each scheduled occurrence runs through the same root procedure path.
schedule due
↓
ProcedureRegistry
↓
ExecutionContext
↓
lazy transaction
↓
commit / rollback
The occurrence is successful only after transaction commit.
Topic publication inside the procedure participates in that same transaction.
59. Invocation capability model
The complete model remains:
CALL synchronous function execution
CREATE EVENT scheduled function execution
TOPIC TRIGGER internal event-driven function execution
CONSUME / ACK external event consumption
These capabilities are complementary.
60. Final design statement
KalamDB should feel like PostgreSQL with first-class realtime, durable topics, generated SDKs, and server-side application procedures — not like a separate application framework bolted onto a database.
The type path is deliberately simple:
SQL type
↓
Kalam catalog / ContractSnapshot
↓
DataFusion / Arrow
↓
KSerializable typed serde when bytes are required
↓
RocksDB / topics / runtime transport
The developer writes:
1. SQL contract
2. business logic code
KalamDB owns:
type resolution
schema evolution
binary serialization
generated models
procedure registration
transactions
topic delivery
runtime isolation
deployment
observability
The central rule remains:
SQL defines the backend contract. DataFusion/Arrow is the shared typed value model. KalamDB reuses one fast serialization stack instead of turning typed values into JSON internally.