Skip to content

Commit 296983b

Browse files
committed
Update examples and TypeScript SDKs
Switch the chat-with-ai example to a package-driven workflow and add Windows usage notes; remove legacy ensure-sdk scripts from examples. Bump local TypeScript SDK versions to 0.5.0-beta.1 and update client/README/quickstart docs and related code (client/types). Update examples (App.tsx and react-ai-chat metadata) and bump dev/runtime dependencies (React, TypeScript, Vite, Playwright, etc.). package-lock.json regenerated to reflect these dependency and SDK changes.
1 parent c294071 commit 296983b

69 files changed

Lines changed: 1768 additions & 2447 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ use kalamdb_commons::models::UserId;
6868
- WASM for browser-based client library
6969

7070
**Job Management System** (Phase 9 + Phase 8.5 Complete):
71-
- **UnifiedJobManager**: Typed JobIds (FL/CL/RT/SE/UC/CO/BK/RS), idempotency, retry logic (3× default with exponential backoff)
72-
- **8 Job Executors**: FlushExecutor (complete), CleanupExecutor (✅), RetentionExecutor (✅), StreamEvictionExecutor (✅), UserCleanupExecutor (✅), CompactExecutor (placeholder), BackupExecutor (placeholder), RestoreExecutor (placeholder)
71+
- **UnifiedJobManager**: Typed JobIds across active and legacy-compatible job types, with idempotency and retry logic (3× default with exponential backoff)
72+
- **Registered Job Executors**: FlushExecutor, CleanupExecutor, StreamEvictionExecutor, CompactExecutor, BackupExecutor, RestoreExecutor, VectorIndexExecutor, TopicRetentionExecutor, and UserExportExecutor. Legacy RT/UC/TC job types remain only for historical `system.jobs` decoding.
7373
- **Status Transitions**: New → Queued → Running → Completed/Failed/Retrying/Cancelled
7474
- **Crash Recovery**: Marks Running jobs as Failed on server restart
7575
- **Idempotency Keys**: Format "{job_type}:{namespace}:{table}:{date}" prevents duplicate jobs

Cargo.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/crates/kalamdb-api/src/http/sql/execution_paths.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ fn build_sql_error_response(
170170

171171
fn build_kalamdb_error_response(err: &KalamDbError, took: f64, is_admin: bool) -> HttpResponse {
172172
let (status, code, preserve_message) = classify_sql_error(err);
173-
build_sql_error_response(status, code, &err.to_string(), None, took, is_admin, preserve_message)
173+
let message = err.user_message();
174+
build_sql_error_response(status, code, message.as_ref(), None, took, is_admin, preserve_message)
174175
}
175176

176177
fn build_statement_error_response(
@@ -182,10 +183,11 @@ fn build_statement_error_response(
182183
) -> HttpResponse {
183184
if let Some(kalamdb_err) = err.downcast_ref::<KalamDbError>() {
184185
let (status, code, preserve_message) = classify_sql_error(kalamdb_err);
186+
let message = kalamdb_err.statement_failure_message(statement_index);
185187
return build_sql_error_response(
186188
status,
187189
code,
188-
&format!("Statement {} failed: {}", statement_index, kalamdb_err),
190+
&message,
189191
Some(sql),
190192
took,
191193
is_admin,
@@ -195,21 +197,23 @@ fn build_statement_error_response(
195197

196198
let err_msg = err.to_string();
197199
if is_leader_routing_error_message(&err_msg.to_lowercase()) {
200+
let message = format!("Statement {statement_index} failed: {err_msg}");
198201
return build_sql_error_response(
199202
StatusCode::SERVICE_UNAVAILABLE,
200203
ErrorCode::NotLeader,
201-
&format!("Statement {} failed: {}", statement_index, err_msg),
204+
&message,
202205
Some(sql),
203206
took,
204207
is_admin,
205208
true,
206209
);
207210
}
208211

212+
let message = format!("Statement {statement_index} failed: {err_msg}");
209213
build_sql_error_response(
210214
StatusCode::BAD_REQUEST,
211215
ErrorCode::SqlExecutionError,
212-
&format!("Statement {} failed: {}", statement_index, err),
216+
&message,
213217
Some(sql),
214218
took,
215219
is_admin,
@@ -370,9 +374,10 @@ pub(super) async fn execute_file_upload_path(
370374
));
371375
},
372376
Err(err) => {
377+
let message = err.to_string();
373378
return HttpResponse::BadRequest().json(SqlResponse::error_for_privilege(
374379
ErrorCode::InvalidSql,
375-
&format!("Failed to parse SQL statement after FILE() substitution: {}", err),
380+
&message,
376381
took_ms(start_time),
377382
exec_ctx.is_admin(),
378383
));

backend/crates/kalamdb-api/src/http/sql/helpers/executor.rs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use std::sync::Arc;
44

5-
use kalamdb_commons::{models::UserId, Role};
5+
use kalamdb_commons::{models::UserId, Role, SqlSubscriptionRow};
66
use kalamdb_core::sql::{
77
context::ExecutionContext,
88
executor::{PreparedExecutionStatement, ScalarValue, SqlExecutor},
@@ -60,18 +60,12 @@ pub fn execution_result_to_query_result(
6060
subscription_id,
6161
channel,
6262
select_query,
63-
} => {
64-
let sub_data = serde_json::json!({
65-
"status": "active",
66-
"ws_url": channel,
67-
"subscription": {
68-
"id": subscription_id,
69-
"sql": select_query
70-
},
71-
"message": "WebSocket subscription created. Connect to ws_url to receive updates."
72-
});
73-
Ok(QueryResult::subscription(sub_data))
74-
},
63+
} => Ok(QueryResult::subscription(SqlSubscriptionRow::new(
64+
subscription_id,
65+
channel,
66+
select_query,
67+
"Subscription created. Connect to ws_url to receive updates.",
68+
))),
7569
ExecutionResult::JobKilled { job_id, status } => {
7670
Ok(QueryResult::with_message(format!("Job {} killed: {}", job_id, status)))
7771
},

backend/crates/kalamdb-api/src/http/sql/models/sql_response.rs

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::fmt;
77
use kalamdb_commons::{
88
models::{datatypes::KalamDataType, KalamCellValue},
99
schemas::SchemaField,
10-
ResponseStatus,
10+
ResponseStatus, SqlSubscriptionRow,
1111
};
1212
use serde::{Deserialize, Serialize, Serializer};
1313

@@ -106,11 +106,17 @@ impl ErrorCode {
106106
#[inline]
107107
fn public_message(&self) -> Option<&'static str> {
108108
match self {
109-
ErrorCode::BatchParseError => Some("Failed to parse SQL batch"),
110-
ErrorCode::SqlExecutionError => Some("SQL statement failed"),
111-
ErrorCode::InvalidSql => Some("SQL statement is invalid or not allowed"),
109+
ErrorCode::BatchParseError => {
110+
Some("Failed to parse SQL batch. Review the SQL and try again.")
111+
},
112+
ErrorCode::SqlExecutionError => {
113+
Some("SQL statement failed. Review the statement and try again.")
114+
},
115+
ErrorCode::InvalidSql => {
116+
Some("SQL statement is invalid or not allowed. Review the SQL and try again.")
117+
},
112118
ErrorCode::TableNotFound => Some("Requested table is not available"),
113-
ErrorCode::InternalError => Some("SQL request failed"),
119+
ErrorCode::InternalError => Some("SQL request failed. Retry the request."),
114120
_ => None,
115121
}
116122
}
@@ -357,29 +363,23 @@ impl QueryResult {
357363
/// Create a result for a SUBSCRIBE TO statement
358364
///
359365
/// Returns subscription metadata as a single row result
360-
pub fn subscription(subscription_data: serde_json::Value) -> Self {
366+
pub fn subscription(subscription_data: SqlSubscriptionRow) -> Self {
361367
let schema = vec![
362368
SchemaField::new("status", KalamDataType::Text, 0),
363369
SchemaField::new("ws_url", KalamDataType::Text, 1),
364370
SchemaField::new("subscription", KalamDataType::Json, 2),
365371
SchemaField::new("message", KalamDataType::Text, 3),
366372
];
367373

368-
// Extract values in schema order
369-
let row = if let serde_json::Value::Object(map) = subscription_data {
370-
vec![
371-
KalamCellValue::from(map.get("status").cloned().unwrap_or(serde_json::Value::Null)),
372-
KalamCellValue::from(map.get("ws_url").cloned().unwrap_or(serde_json::Value::Null)),
373-
KalamCellValue::from(
374-
map.get("subscription").cloned().unwrap_or(serde_json::Value::Null),
375-
),
376-
KalamCellValue::from(
377-
map.get("message").cloned().unwrap_or(serde_json::Value::Null),
378-
),
379-
]
380-
} else {
381-
vec![KalamCellValue::null(); 4]
382-
};
374+
let row = vec![
375+
KalamCellValue::text(subscription_data.status.as_str()),
376+
KalamCellValue::text(subscription_data.ws_url),
377+
KalamCellValue::from(
378+
serde_json::to_value(subscription_data.subscription)
379+
.expect("SQL subscription descriptor should serialize"),
380+
),
381+
KalamCellValue::text(subscription_data.message),
382+
];
383383

384384
Self {
385385
schema,
@@ -444,6 +444,30 @@ mod tests {
444444
assert!(json.contains("\"data_type\":\"Timestamp\""));
445445
}
446446

447+
#[test]
448+
fn test_subscription_result_uses_shared_row_type() {
449+
let result = QueryResult::subscription(SqlSubscriptionRow::new(
450+
"sub-1",
451+
"ws://localhost:8080/v1/ws",
452+
"SELECT * FROM chat.messages",
453+
"Subscription created. Connect to ws_url to receive updates.",
454+
));
455+
456+
let rows = result.rows.as_ref().expect("subscription result should include a row");
457+
assert_eq!(rows[0][0].as_str(), Some("subscription_required"));
458+
assert_eq!(rows[0][1].as_str(), Some("ws://localhost:8080/v1/ws"));
459+
assert_eq!(rows[0][3].as_str(), Some("Subscription created. Connect to ws_url to receive updates."));
460+
461+
let subscription = rows[0][2]
462+
.as_object()
463+
.expect("subscription column should be a JSON object");
464+
assert_eq!(subscription.get("id").and_then(|value| value.as_str()), Some("sub-1"));
465+
assert_eq!(
466+
subscription.get("sql").and_then(|value| value.as_str()),
467+
Some("SELECT * FROM chat.messages")
468+
);
469+
}
470+
447471
#[test]
448472
fn test_column_names() {
449473
let schema = vec![
@@ -505,7 +529,10 @@ mod tests {
505529

506530
let error = response.error.expect("error response should include an error payload");
507531
assert_eq!(error.code, ErrorCode::SqlExecutionError);
508-
assert_eq!(error.message, "SQL statement failed");
532+
assert_eq!(
533+
error.message,
534+
"SQL statement failed. Review the statement and try again.",
535+
);
509536
assert!(error.details.is_none());
510537
}
511538

backend/crates/kalamdb-api/src/http/sql/statements.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,10 @@ pub(super) fn split_and_prepare_statements(
185185
}
186186

187187
let raw_statements = kalamdb_sql::split_statements(sql).map_err(|err| {
188+
let message = err.to_string();
188189
HttpResponse::BadRequest().json(SqlResponse::error_for_privilege(
189190
ErrorCode::BatchParseError,
190-
&format!("Failed to parse SQL batch: {}", err),
191+
&message,
191192
took_ms(start_time),
192193
exec_ctx.is_admin(),
193194
))

backend/crates/kalamdb-api/src/ws/events/subscription.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ pub async fn handle_subscribe(
199199
kalamdb_live::error::LiveError::InvalidOperation(_) => WsErrorCode::Unsupported,
200200
_ => WsErrorCode::SubscriptionFailed,
201201
};
202-
let message = e.to_string();
202+
let message = e.user_message();
203203
// Use warn for "expected" client errors (table gone, bad SQL) to avoid
204204
// flooding logs during benchmark teardown; keep error for server-side issues.
205205
match &code {
@@ -216,8 +216,14 @@ pub async fn handle_subscribe(
216216
);
217217
},
218218
}
219-
let _ =
220-
send_error(session, &subscription_id, code, &message, compression_enabled).await;
219+
let _ = send_error(
220+
session,
221+
&subscription_id,
222+
code,
223+
message.as_ref(),
224+
compression_enabled,
225+
)
226+
.await;
221227
Ok(())
222228
},
223229
}

backend/crates/kalamdb-commons/src/api_models.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use serde::{Deserialize, Serialize};
22

3+
use crate::websocket_messages::SubscriptionOptions;
4+
35
/// Health check response from the server.
46
#[derive(Debug, Clone, Serialize, Deserialize)]
57
pub struct HealthCheckResponse {
@@ -71,3 +73,70 @@ impl std::fmt::Display for ResponseStatus {
7173
}
7274
}
7375
}
76+
77+
/// Status values returned by `SUBSCRIBE TO` over the SQL HTTP API.
78+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
79+
#[serde(rename_all = "snake_case")]
80+
pub enum SqlSubscriptionStatus {
81+
SubscriptionRequired,
82+
Active,
83+
}
84+
85+
impl SqlSubscriptionStatus {
86+
pub fn as_str(self) -> &'static str {
87+
match self {
88+
SqlSubscriptionStatus::SubscriptionRequired => "subscription_required",
89+
SqlSubscriptionStatus::Active => "active",
90+
}
91+
}
92+
}
93+
94+
impl std::fmt::Display for SqlSubscriptionStatus {
95+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96+
write!(f, "{}", self.as_str())
97+
}
98+
}
99+
100+
/// Nested subscription metadata returned by `SUBSCRIBE TO` over SQL HTTP.
101+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
102+
pub struct SqlSubscriptionDescriptor {
103+
pub id: String,
104+
pub sql: String,
105+
#[serde(default, skip_serializing_if = "Option::is_none")]
106+
pub options: Option<SubscriptionOptions>,
107+
}
108+
109+
impl SqlSubscriptionDescriptor {
110+
pub fn new(id: impl Into<String>, sql: impl Into<String>) -> Self {
111+
Self {
112+
id: id.into(),
113+
sql: sql.into(),
114+
options: None,
115+
}
116+
}
117+
}
118+
119+
/// Single-row payload returned for `SUBSCRIBE TO` via `/v1/api/sql`.
120+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
121+
pub struct SqlSubscriptionRow {
122+
pub status: SqlSubscriptionStatus,
123+
pub ws_url: String,
124+
pub subscription: SqlSubscriptionDescriptor,
125+
pub message: String,
126+
}
127+
128+
impl SqlSubscriptionRow {
129+
pub fn new(
130+
subscription_id: impl Into<String>,
131+
ws_url: impl Into<String>,
132+
sql: impl Into<String>,
133+
message: impl Into<String>,
134+
) -> Self {
135+
Self {
136+
status: SqlSubscriptionStatus::SubscriptionRequired,
137+
ws_url: ws_url.into(),
138+
subscription: SqlSubscriptionDescriptor::new(subscription_id, sql),
139+
message: message.into(),
140+
}
141+
}
142+
}

backend/crates/kalamdb-commons/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ extern crate self as kalamdb_commons;
7272
// Re-export commonly used types at crate root
7373
pub use api_models::{
7474
ClusterHealthResponse, ClusterNodeHealth, HealthCheckResponse, ResponseStatus,
75+
SqlSubscriptionDescriptor, SqlSubscriptionRow, SqlSubscriptionStatus,
7576
};
7677
pub use constants::{MAX_SQL_QUERY_LENGTH, RESERVED_NAMESPACE_NAMES};
7778
#[cfg(feature = "conversions")]

backend/crates/kalamdb-core/src/cluster_handler.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,7 @@ impl CoreClusterHandler {
194194
rows: None,
195195
row_count: 1,
196196
message: Some(format!(
197-
"Subscription {} on channel {} for query: {}",
198-
subscription_id, channel, select_query
197+
"Subscription {subscription_id} created. Connect to {channel} to receive updates for query: {select_query}"
199198
)),
200199
as_user: as_user.to_string(),
201200
}),
@@ -350,10 +349,11 @@ impl ClusterMessageHandler for CoreClusterHandler {
350349
));
351350
},
352351
Err(e) => {
352+
let message = e.to_string();
353353
return Ok(Self::error_payload(
354354
400,
355355
"BATCH_PARSE_ERROR",
356-
&format!("Failed to parse SQL batch: {}", e),
356+
&message,
357357
started_at,
358358
));
359359
},
@@ -409,7 +409,7 @@ impl ClusterMessageHandler for CoreClusterHandler {
409409
return Ok(Self::error_payload(
410410
400,
411411
"SQL_EXECUTION_ERROR",
412-
&format!("Statement {} failed: {}", idx + 1, e),
412+
&e.statement_failure_message(idx + 1),
413413
started_at,
414414
));
415415
},
@@ -461,7 +461,7 @@ impl ClusterMessageHandler for CoreClusterHandler {
461461
return Ok(Self::error_payload(
462462
400,
463463
"SQL_EXECUTION_ERROR",
464-
&format!("Statement {} failed: {}", idx + 1, e),
464+
&e.statement_failure_message(idx + 1),
465465
started_at,
466466
));
467467
},

0 commit comments

Comments
 (0)