1#![expect(
18 clippy::disallowed_types,
19 reason = "construct iceberg::Error to implement the trait"
20)]
21
22use std::collections::HashMap;
23use std::fmt::Debug;
24use std::sync::Arc;
25
26use anyhow::Context;
27use async_trait::async_trait;
28use iceberg::io::FileIOBuilder;
29use iceberg::spec::{Schema, SortOrder, TableMetadata, UnboundPartitionSpec};
30use iceberg::table::Table;
31use iceberg::{
32 Catalog, Namespace, NamespaceIdent, Runtime, TableCommit, TableCreation, TableIdent,
33 TableRequirement, TableUpdate,
34};
35use iceberg_storage_opendal::OpenDalResolvingStorageFactory;
36use itertools::Itertools;
37use jni::objects::{GlobalRef, JObject};
38use risingwave_common::global_jvm::Jvm;
39use risingwave_jni_core::call_method;
40use risingwave_jni_core::jvm_runtime::{execute_with_jni_env, jobj_to_str};
41use serde::{Deserialize, Serialize};
42use thiserror_ext::AsReport;
43
44use crate::error::ConnectorResult;
45
46#[derive(Debug, Deserialize)]
47#[serde(rename_all = "kebab-case")]
48struct LoadTableResponse {
49 pub metadata_location: Option<String>,
50 pub metadata: TableMetadata,
51 pub _config: Option<HashMap<String, String>>,
52}
53
54#[derive(Debug, Serialize)]
55#[serde(rename_all = "kebab-case")]
56struct CreateTableRequest {
57 pub name: String,
59 pub location: Option<String>,
61 pub schema: Schema,
63 pub partition_spec: Option<UnboundPartitionSpec>,
65 pub write_order: Option<SortOrder>,
67 pub properties: HashMap<String, String>,
69}
70
71#[derive(Debug, Serialize, Deserialize)]
72struct CommitTableRequest {
73 identifier: TableIdent,
74 requirements: Vec<TableRequirement>,
75 updates: Vec<TableUpdate>,
76}
77
78#[derive(Debug, Serialize, Deserialize)]
79#[serde(rename_all = "kebab-case")]
80struct CommitTableResponse {
81 metadata_location: String,
82 metadata: TableMetadata,
83}
84
85#[derive(Debug, Serialize, Deserialize)]
86#[serde(rename_all = "kebab-case")]
87struct ListNamespacesResponse {
88 namespaces: Vec<NamespaceIdent>,
89 next_page_token: Option<String>,
90}
91
92#[derive(Debug, Serialize, Deserialize)]
93#[serde(rename_all = "kebab-case")]
94struct ListTablesResponse {
95 identifiers: Vec<TableIdent>,
96 next_page_token: Option<String>,
97}
98
99impl From<&TableCreation> for CreateTableRequest {
100 fn from(value: &TableCreation) -> Self {
101 Self {
102 name: value.name.clone(),
103 location: value.location.clone(),
104 schema: value.schema.clone(),
105 partition_spec: value.partition_spec.clone(),
106 write_order: value.sort_order.clone(),
107 properties: value.properties.clone(),
108 }
109 }
110}
111
112fn namespace_to_string(namespace: &NamespaceIdent) -> String {
113 namespace.iter().join(".")
114}
115
116#[derive(Debug)]
117struct JniCatalogInner {
118 java_catalog: GlobalRef,
119 jvm: Jvm,
120}
121
122#[derive(Debug)]
123pub struct JniCatalog {
124 inner: Arc<JniCatalogInner>,
127 file_io_props: Arc<HashMap<String, String>>,
128}
129
130async fn execute_blocking_jni<T>(
134 task: impl FnOnce() -> anyhow::Result<T> + Send + 'static,
135) -> anyhow::Result<T>
136where
137 T: Send + 'static,
138{
139 tokio::task::spawn_blocking(task)
140 .await
141 .context("Failed to join blocking Iceberg JNI catalog task")?
142}
143
144#[async_trait]
145impl Catalog for JniCatalog {
146 async fn list_namespaces(
148 &self,
149 _parent: Option<&NamespaceIdent>,
150 ) -> iceberg::Result<Vec<NamespaceIdent>> {
151 let inner = self.inner.clone();
152 execute_blocking_jni(move || {
153 execute_with_jni_env(inner.jvm, |env| {
154 let result_json =
155 call_method!(env, inner.java_catalog.as_obj(), {String listNamespaces()})
156 .with_context(|| "Failed to list iceberg namespaces".to_owned())?;
157
158 let rust_json_str = jobj_to_str(env, result_json)?;
159
160 let resp: ListNamespacesResponse = serde_json::from_str(&rust_json_str)?;
161
162 Ok(resp.namespaces)
163 })
164 })
165 .await
166 .map_err(|e| {
167 iceberg::Error::new(
168 iceberg::ErrorKind::Unexpected,
169 "Failed to list iceberg namespaces.",
170 )
171 .with_source(e)
172 })
173 }
174
175 async fn create_namespace(
177 &self,
178 namespace: &iceberg::NamespaceIdent,
179 _properties: HashMap<String, String>,
180 ) -> iceberg::Result<iceberg::Namespace> {
181 let inner = self.inner.clone();
182 let namespace = namespace.clone();
183 execute_blocking_jni(move || {
184 execute_with_jni_env(inner.jvm, |env| {
185 let namespace_str = namespace_to_string(&namespace);
186 let namespace_jstr = env.new_string(&namespace_str).unwrap();
187
188 call_method!(env, inner.java_catalog.as_obj(), {void createNamespace(String)},
189 &namespace_jstr)
190 .with_context(|| format!("Failed to create namespace: {namespace}"))?;
191
192 Ok(Namespace::new(namespace))
193 })
194 })
195 .await
196 .map_err(|e| {
197 iceberg::Error::new(
198 iceberg::ErrorKind::Unexpected,
199 "Failed to create namespace.",
200 )
201 .with_source(e)
202 })
203 }
204
205 async fn get_namespace(&self, _namespace: &NamespaceIdent) -> iceberg::Result<Namespace> {
207 todo!()
208 }
209
210 async fn namespace_exists(&self, namespace: &NamespaceIdent) -> iceberg::Result<bool> {
212 let inner = self.inner.clone();
213 let namespace = namespace.clone();
214 execute_blocking_jni(move || {
215 execute_with_jni_env(inner.jvm, |env| {
216 let namespace_str = namespace_to_string(&namespace);
217 let namespace_jstr = env.new_string(&namespace_str).unwrap();
218
219 let exists =
220 call_method!(env, inner.java_catalog.as_obj(), {boolean namespaceExists(String)},
221 &namespace_jstr)
222 .with_context(|| format!("Failed to check namespace exists: {namespace}"))?;
223
224 Ok(exists)
225 })
226 })
227 .await
228 .map_err(|e| {
229 iceberg::Error::new(
230 iceberg::ErrorKind::Unexpected,
231 "Failed to check namespace exists.",
232 )
233 .with_source(e)
234 })
235 }
236
237 async fn drop_namespace(&self, _namespace: &NamespaceIdent) -> iceberg::Result<()> {
239 todo!()
240 }
241
242 async fn list_tables(&self, namespace: &NamespaceIdent) -> iceberg::Result<Vec<TableIdent>> {
244 let inner = self.inner.clone();
245 let namespace = namespace.clone();
246 execute_blocking_jni(move || {
247 execute_with_jni_env(inner.jvm, |env| {
248 let namespace_str = namespace_to_string(&namespace);
249 let namespace_jstr = env.new_string(&namespace_str).unwrap();
250
251 let result_json =
252 call_method!(env, inner.java_catalog.as_obj(), {String listTables(String)},
253 &namespace_jstr)
254 .with_context(|| {
255 format!("Failed to list iceberg tables in namespace: {}", namespace)
256 })?;
257
258 let rust_json_str = jobj_to_str(env, result_json)?;
259
260 let resp: ListTablesResponse = serde_json::from_str(&rust_json_str)?;
261
262 Ok(resp.identifiers)
263 })
264 })
265 .await
266 .map_err(|e| {
267 iceberg::Error::new(
268 iceberg::ErrorKind::Unexpected,
269 "Failed to list iceberg tables.",
270 )
271 .with_source(e)
272 })
273 }
274
275 async fn update_namespace(
276 &self,
277 _namespace: &NamespaceIdent,
278 _properties: HashMap<String, String>,
279 ) -> iceberg::Result<()> {
280 todo!()
281 }
282
283 async fn create_table(
285 &self,
286 namespace: &NamespaceIdent,
287 creation: TableCreation,
288 ) -> iceberg::Result<Table> {
289 let inner = self.inner.clone();
290 let file_io_props = self.file_io_props.clone();
291 let runtime = Runtime::try_current()?;
292 let namespace = namespace.clone();
293 execute_blocking_jni(move || {
294 execute_with_jni_env(inner.jvm, |env| {
295 let namespace_str = namespace_to_string(&namespace);
296 let namespace_jstr = env.new_string(&namespace_str).unwrap();
297
298 let creation_str = serde_json::to_string(&CreateTableRequest::from(&creation))?;
299
300 let creation_jstr = env.new_string(&creation_str).unwrap();
301
302 let result_json =
303 call_method!(env, inner.java_catalog.as_obj(), {String createTable(String, String)},
304 &namespace_jstr, &creation_jstr)
305 .with_context(|| {
306 format!("Failed to create iceberg table: {}", creation.name)
307 })?;
308
309 let rust_json_str = jobj_to_str(env, result_json)?;
310
311 let resp: LoadTableResponse = serde_json::from_str(&rust_json_str)?;
312
313 let _metadata_location = resp.metadata_location.ok_or_else(|| {
314 iceberg::Error::new(
315 iceberg::ErrorKind::FeatureUnsupported,
316 "Loading uncommitted table is not supported!",
317 )
318 })?;
319
320 let table_metadata = resp.metadata;
321
322 let file_io =
323 FileIOBuilder::new(Arc::new(OpenDalResolvingStorageFactory::new()))
324 .with_props(file_io_props.iter())
325 .build();
326
327 Ok(Table::builder()
328 .file_io(file_io)
329 .identifier(TableIdent::new(namespace, creation.name))
330 .metadata(table_metadata)
331 .runtime(runtime)
332 .build())
333 })
334 })
335 .await
336 .map_err(|e| {
337 iceberg::Error::new(
338 iceberg::ErrorKind::Unexpected,
339 "Failed to create iceberg table.",
340 )
341 .with_source(e)
342 })?
343 }
344
345 async fn load_table(&self, table: &TableIdent) -> iceberg::Result<Table> {
347 let inner = self.inner.clone();
348 let file_io_props = self.file_io_props.clone();
349 let runtime = Runtime::try_current()?;
350 let table = table.clone();
351 execute_blocking_jni(move || {
352 execute_with_jni_env(inner.jvm, |env| {
353 let table_name_str = table.to_string();
354
355 let table_name_jstr = env.new_string(&table_name_str).unwrap();
356
357 let result_json =
358 call_method!(env, inner.java_catalog.as_obj(), {String loadTable(String)},
359 &table_name_jstr)
360 .with_context(|| format!("Failed to load iceberg table: {table_name_str}"))?;
361
362 let rust_json_str = jobj_to_str(env, result_json)?;
363
364 let resp: LoadTableResponse = serde_json::from_str(&rust_json_str)?;
365
366 let metadata_location = resp.metadata_location.ok_or_else(|| {
367 iceberg::Error::new(
368 iceberg::ErrorKind::FeatureUnsupported,
369 "Loading uncommitted table is not supported!",
370 )
371 })?;
372
373 tracing::info!(
374 "Table metadata location of {table_name_str} is {metadata_location}"
375 );
376
377 let table_metadata = resp.metadata;
378
379 let file_io = FileIOBuilder::new(Arc::new(OpenDalResolvingStorageFactory::new()))
380 .with_props(file_io_props.iter())
381 .build();
382
383 Ok(Table::builder()
384 .file_io(file_io)
385 .identifier(table)
386 .metadata(table_metadata)
387 .runtime(runtime)
388 .build())
389 })
390 })
391 .await
392 .map_err(|e| {
393 iceberg::Error::new(
394 iceberg::ErrorKind::Unexpected,
395 "Failed to load iceberg table.",
396 )
397 .with_source(e)
398 })?
399 }
400
401 async fn drop_table(&self, table: &TableIdent) -> iceberg::Result<()> {
403 let inner = self.inner.clone();
404 let table = table.to_owned();
405 execute_blocking_jni(move || {
406 execute_with_jni_env(inner.jvm, |env| {
407 let table_name_str = table.to_string();
408
409 let table_name_jstr = env.new_string(&table_name_str).unwrap();
410
411 call_method!(env, inner.java_catalog.as_obj(), {boolean dropTable(String)},
412 &table_name_jstr)
413 .with_context(|| format!("Failed to drop iceberg table: {table_name_str}"))?;
414
415 Ok(())
416 })
417 })
418 .await
419 .map_err(|e| {
420 iceberg::Error::new(
421 iceberg::ErrorKind::Unexpected,
422 "Failed to drop iceberg table.",
423 )
424 .with_source(e)
425 })
426 }
427
428 async fn purge_table(&self, table: &TableIdent) -> iceberg::Result<()> {
429 let table_info = self.load_table(table).await?;
430 self.drop_table(table).await?;
431 iceberg::drop_table_data(&table_info).await
432 }
433
434 async fn register_table(
435 &self,
436 _table_ident: &TableIdent,
437 _metadata_location: String,
438 ) -> iceberg::Result<Table> {
439 Err(iceberg::Error::new(
440 iceberg::ErrorKind::Unexpected,
441 "register_table is not supported by JniCatalog",
442 ))
443 }
444
445 async fn table_exists(&self, table: &TableIdent) -> iceberg::Result<bool> {
447 let inner = self.inner.clone();
448 let table = table.clone();
449 execute_blocking_jni(move || {
450 execute_with_jni_env(inner.jvm, |env| {
451 let table_name_str = table.to_string();
452
453 let table_name_jstr = env.new_string(&table_name_str).unwrap();
454
455 let exists =
456 call_method!(env, inner.java_catalog.as_obj(), {boolean tableExists(String)},
457 &table_name_jstr)
458 .with_context(|| {
459 format!("Failed to check iceberg table exists: {table_name_str}")
460 })?;
461
462 Ok(exists)
463 })
464 })
465 .await
466 .map_err(|e| {
467 iceberg::Error::new(
468 iceberg::ErrorKind::Unexpected,
469 "Failed to check iceberg table exists.",
470 )
471 .with_source(e)
472 })
473 }
474
475 async fn rename_table(&self, _src: &TableIdent, _dest: &TableIdent) -> iceberg::Result<()> {
477 todo!()
478 }
479
480 async fn update_table(&self, mut commit: TableCommit) -> iceberg::Result<Table> {
482 let inner = self.inner.clone();
483 let file_io_props = self.file_io_props.clone();
484 let runtime = Runtime::try_current()?;
485 execute_blocking_jni(move || {
486 execute_with_jni_env(inner.jvm, |env| {
487 let requirements = commit.take_requirements();
488 let updates = commit.take_updates();
489 let request = CommitTableRequest {
490 identifier: commit.identifier().clone(),
491 requirements,
492 updates,
493 };
494 let request_str = serde_json::to_string(&request)?;
495
496 let request_jni_str = env.new_string(&request_str).with_context(|| {
497 format!("Failed to create jni string from request json: {request_str}.")
498 })?;
499
500 let result_json =
501 call_method!(env, inner.java_catalog.as_obj(), {String updateTable(String)},
502 &request_jni_str)
503 .with_context(|| {
504 format!("Failed to update iceberg table: {}", commit.identifier())
505 })?;
506
507 let rust_json_str = jobj_to_str(env, result_json)?;
508
509 let response: CommitTableResponse = serde_json::from_str(&rust_json_str)?;
510
511 tracing::info!(
512 "Table metadata location of {} is {}",
513 commit.identifier(),
514 response.metadata_location
515 );
516
517 let table_metadata = response.metadata;
518
519 let file_io = FileIOBuilder::new(Arc::new(OpenDalResolvingStorageFactory::new()))
520 .with_props(file_io_props.iter())
521 .build();
522
523 Ok(Table::builder()
524 .file_io(file_io)
525 .identifier(commit.identifier().clone())
526 .metadata(table_metadata)
527 .runtime(runtime)
528 .build()?)
529 })
530 })
531 .await
532 .map_err(|e| {
533 iceberg::Error::new(
534 iceberg::ErrorKind::Unexpected,
535 "Failed to update iceberg table.",
536 )
537 .with_source(e)
538 })
539 }
540}
541
542impl Drop for JniCatalogInner {
543 fn drop(&mut self) {
544 let _ = execute_with_jni_env(self.jvm, |env| {
545 call_method!(env, self.java_catalog.as_obj(), {void close()})
546 .with_context(|| "Failed to close iceberg catalog".to_owned())?;
547 Ok(())
548 })
549 .inspect_err(
550 |e| tracing::error!(error = ?e.as_report(), "Failed to close iceberg catalog"),
551 );
552 }
553}
554
555impl JniCatalog {
556 fn build(
557 file_io_props: HashMap<String, String>,
558 name: impl ToString,
559 catalog_impl: impl ToString,
560 java_catalog_props: HashMap<String, String>,
561 ) -> ConnectorResult<Self> {
562 let jvm = Jvm::get_or_init()?;
563
564 execute_with_jni_env(jvm, |env| {
565 let props = env.new_object_array(
567 (java_catalog_props.len() * 2) as i32,
568 "java/lang/String",
569 JObject::null(),
570 )?;
571 for (i, (key, value)) in java_catalog_props.iter().enumerate() {
572 let key_j_str = env.new_string(key)?;
573 let value_j_str = env.new_string(value)?;
574 env.set_object_array_element(&props, i as i32 * 2, key_j_str)?;
575 env.set_object_array_element(&props, i as i32 * 2 + 1, value_j_str)?;
576 }
577
578 let jni_catalog_wrapper = env
579 .call_static_method(
580 "com/risingwave/connector/catalog/JniCatalogWrapper",
581 "create",
582 "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)Lcom/risingwave/connector/catalog/JniCatalogWrapper;",
583 &[
584 (&env.new_string(name.to_string()).unwrap()).into(),
585 (&env.new_string(catalog_impl.to_string()).unwrap()).into(),
586 (&props).into(),
587 ],
588 )?;
589
590 let jni_catalog = env.new_global_ref(jni_catalog_wrapper.l().unwrap())?;
591
592 Ok(Self {
593 inner: Arc::new(JniCatalogInner {
594 java_catalog: jni_catalog,
595 jvm,
596 }),
597 file_io_props: Arc::new(file_io_props),
598 })
599 })
600 .map_err(Into::into)
601 }
602
603 pub async fn build_catalog(
604 file_io_props: HashMap<String, String>,
605 name: impl ToString + Send + 'static,
606 catalog_impl: impl ToString + Send + 'static,
607 java_catalog_props: HashMap<String, String>,
608 ) -> ConnectorResult<Arc<dyn Catalog>> {
609 let catalog = execute_blocking_jni(move || {
610 Ok(Self::build(
611 file_io_props,
612 name,
613 catalog_impl,
614 java_catalog_props,
615 )?)
616 })
617 .await?;
618 Ok(Arc::new(catalog) as Arc<dyn Catalog>)
619 }
620}