@@ -61,6 +61,37 @@ void setupCatalog() {
6161 this .flussIcebergCatalog = new IcebergLakeCatalog (configuration );
6262 }
6363
64+ /** Verify property prefix rewriting. */
65+ @ Test
66+ void testPropertyPrefixRewriting () {
67+ String database = "test_db" ;
68+ String tableName = "test_table" ;
69+
70+ Schema flussSchema = Schema .newBuilder ().column ("id" , DataTypes .BIGINT ()).build ();
71+
72+ TableDescriptor tableDescriptor =
73+ TableDescriptor .builder ()
74+ .schema (flussSchema )
75+ .distributedBy (3 )
76+ .property ("iceberg.commit.retry.num-retries" , "5" )
77+ .property ("table.datalake.freshness" , "30s" )
78+ .build ();
79+
80+ TablePath tablePath = TablePath .of (database , tableName );
81+ flussIcebergCatalog .createTable (tablePath , tableDescriptor );
82+
83+ Table created =
84+ flussIcebergCatalog
85+ .getIcebergCatalog ()
86+ .loadTable (TableIdentifier .of (database , tableName ));
87+
88+ // Verify property prefix rewriting
89+ assertThat (created .properties ()).containsEntry ("commit.retry.num-retries" , "5" );
90+ assertThat (created .properties ()).containsEntry ("fluss.table.datalake.freshness" , "30s" );
91+ assertThat (created .properties ())
92+ .doesNotContainKeys ("iceberg.commit.retry.num-retries" , "table.datalake.freshness" );
93+ }
94+
6495 @ Test
6596 void testCreatePrimaryKeyTable () {
6697 String database = "test_db" ;
@@ -217,4 +248,151 @@ void rejectsPrimaryKeyTableWithMultipleBucketKeys() {
217248 .isInstanceOf (UnsupportedOperationException .class )
218249 .hasMessageContaining ("Only one bucket key is supported for Iceberg" );
219250 }
251+
252+ @ Test
253+ void testCreateLogTable () {
254+ String database = "test_db" ;
255+ String tableName = "log_table" ;
256+
257+ Schema flussSchema =
258+ Schema .newBuilder ()
259+ .column ("id" , DataTypes .BIGINT ())
260+ .column ("name" , DataTypes .STRING ())
261+ .column ("amount" , DataTypes .INT ())
262+ .column ("address" , DataTypes .STRING ())
263+ .build ();
264+
265+ TableDescriptor td =
266+ TableDescriptor .builder ()
267+ .schema (flussSchema )
268+ .distributedBy (3 ) // no bucket key
269+ .build ();
270+
271+ TablePath tablePath = TablePath .of (database , tableName );
272+ flussIcebergCatalog .createTable (tablePath , td );
273+
274+ TableIdentifier tableId = TableIdentifier .of (database , tableName );
275+ Table createdTable = flussIcebergCatalog .getIcebergCatalog ().loadTable (tableId );
276+
277+ org .apache .iceberg .Schema expectIcebergSchema =
278+ new org .apache .iceberg .Schema (
279+ Arrays .asList (
280+ Types .NestedField .optional (1 , "id" , Types .LongType .get ()),
281+ Types .NestedField .optional (2 , "name" , Types .StringType .get ()),
282+ Types .NestedField .optional (3 , "amount" , Types .IntegerType .get ()),
283+ Types .NestedField .optional (4 , "address" , Types .StringType .get ()),
284+ Types .NestedField .required (
285+ 5 , BUCKET_COLUMN_NAME , Types .IntegerType .get ()),
286+ Types .NestedField .required (
287+ 6 , OFFSET_COLUMN_NAME , Types .LongType .get ()),
288+ Types .NestedField .required (
289+ 7 , TIMESTAMP_COLUMN_NAME , Types .TimestampType .withZone ())));
290+
291+ // Verify iceberg table schema
292+ assertThat (createdTable .schema ().toString ()).isEqualTo (expectIcebergSchema .toString ());
293+
294+ // Verify partition field and transform
295+ assertThat (createdTable .spec ().fields ()).hasSize (1 );
296+ PartitionField partitionField = createdTable .spec ().fields ().get (0 );
297+ assertThat (partitionField .name ()).isEqualTo (BUCKET_COLUMN_NAME );
298+ assertThat (partitionField .transform ().toString ()).isEqualTo ("identity" );
299+
300+ // Verify sort field and order
301+ assertThat (createdTable .sortOrder ().fields ()).hasSize (1 );
302+ SortField sortField = createdTable .sortOrder ().fields ().get (0 );
303+ assertThat (sortField .sourceId ())
304+ .isEqualTo (createdTable .schema ().findField (OFFSET_COLUMN_NAME ).fieldId ());
305+ assertThat (sortField .direction ()).isEqualTo (SortDirection .ASC );
306+ }
307+
308+ @ Test
309+ void testCreatePartitionedLogTable () {
310+ String database = "test_db" ;
311+ String tableName = "partitioned_log_table" ;
312+
313+ Schema flussSchema =
314+ Schema .newBuilder ()
315+ .column ("id" , DataTypes .BIGINT ())
316+ .column ("name" , DataTypes .STRING ())
317+ .column ("amount" , DataTypes .INT ())
318+ .column ("order_type" , DataTypes .STRING ())
319+ .build ();
320+
321+ TableDescriptor td =
322+ TableDescriptor .builder ()
323+ .schema (flussSchema )
324+ .distributedBy (3 )
325+ .partitionedBy ("order_type" )
326+ .build ();
327+
328+ TablePath path = TablePath .of (database , tableName );
329+ flussIcebergCatalog .createTable (path , td );
330+
331+ Table createdTable =
332+ flussIcebergCatalog
333+ .getIcebergCatalog ()
334+ .loadTable (TableIdentifier .of (database , tableName ));
335+
336+ org .apache .iceberg .Schema expectIcebergSchema =
337+ new org .apache .iceberg .Schema (
338+ Arrays .asList (
339+ Types .NestedField .optional (1 , "id" , Types .LongType .get ()),
340+ Types .NestedField .optional (2 , "name" , Types .StringType .get ()),
341+ Types .NestedField .optional (3 , "amount" , Types .IntegerType .get ()),
342+ Types .NestedField .optional (4 , "order_type" , Types .StringType .get ()),
343+ Types .NestedField .required (
344+ 5 , BUCKET_COLUMN_NAME , Types .IntegerType .get ()),
345+ Types .NestedField .required (
346+ 6 , OFFSET_COLUMN_NAME , Types .LongType .get ()),
347+ Types .NestedField .required (
348+ 7 , TIMESTAMP_COLUMN_NAME , Types .TimestampType .withZone ())));
349+
350+ // Verify iceberg table schema
351+ assertThat (createdTable .schema ().toString ()).isEqualTo (expectIcebergSchema .toString ());
352+
353+ // Verify partition field and transform
354+ assertThat (createdTable .spec ().fields ()).hasSize (2 );
355+ PartitionField firstPartitionField = createdTable .spec ().fields ().get (0 );
356+ assertThat (firstPartitionField .name ()).isEqualTo ("order_type" );
357+ assertThat (firstPartitionField .transform ().toString ()).isEqualTo ("identity" );
358+
359+ PartitionField secondPartitionField = createdTable .spec ().fields ().get (1 );
360+ assertThat (secondPartitionField .name ()).isEqualTo (BUCKET_COLUMN_NAME );
361+ assertThat (secondPartitionField .transform ().toString ()).isEqualTo ("identity" );
362+
363+ // Verify sort field and order
364+ assertThat (createdTable .sortOrder ().fields ()).hasSize (1 );
365+ SortField sortField = createdTable .sortOrder ().fields ().get (0 );
366+ assertThat (sortField .sourceId ())
367+ .isEqualTo (createdTable .schema ().findField (OFFSET_COLUMN_NAME ).fieldId ());
368+ assertThat (sortField .direction ()).isEqualTo (SortDirection .ASC );
369+ }
370+
371+ @ Test
372+ void rejectsLogTableWithMultipleBucketKeys () {
373+ String database = "test_db" ;
374+ String tableName = "multi_bucket_log_table" ;
375+
376+ Schema flussSchema =
377+ Schema .newBuilder ()
378+ .column ("id" , DataTypes .BIGINT ())
379+ .column ("name" , DataTypes .STRING ())
380+ .column ("amount" , DataTypes .INT ())
381+ .column ("user_type" , DataTypes .STRING ())
382+ .column ("order_type" , DataTypes .STRING ())
383+ .build ();
384+
385+ TableDescriptor tableDescriptor =
386+ TableDescriptor .builder ()
387+ .schema (flussSchema )
388+ .distributedBy (3 , "user_type" , "order_type" )
389+ .build ();
390+
391+ TablePath tablePath = TablePath .of (database , tableName );
392+
393+ // Do not allow multiple bucket keys for log table
394+ assertThatThrownBy (() -> flussIcebergCatalog .createTable (tablePath , tableDescriptor ))
395+ .isInstanceOf (UnsupportedOperationException .class )
396+ .hasMessageContaining ("Only one bucket key is supported for Iceberg" );
397+ }
220398}
0 commit comments