laserninja commented on code in PR #11206: URL: https://github.com/apache/gravitino/pull/11206#discussion_r3647644693
########## maintenance/jobs/src/test/java/org/apache/gravitino/maintenance/jobs/iceberg/TestIcebergExpireSnapshotsJob.java: ########## @@ -0,0 +1,506 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.maintenance.jobs.iceberg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Map; +import org.apache.gravitino.job.JobTemplateProvider; +import org.apache.gravitino.job.SparkJobTemplate; +import org.junit.jupiter.api.Test; + +public class TestIcebergExpireSnapshotsJob { + + @Test + public void testJobTemplateHasCorrectName() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template); + assertEquals("builtin-iceberg-expire-snapshots", template.name()); + } + + @Test + public void testJobTemplateHasComment() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.comment()); + assertFalse(template.comment().trim().isEmpty()); + assertTrue(template.comment().contains("Iceberg")); + } + + @Test + public void testJobTemplateHasExecutable() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.executable()); + assertFalse(template.executable().trim().isEmpty()); + } + + @Test + public void testJobTemplateHasMainClass() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.className()); + assertEquals(IcebergExpireSnapshotsJob.class.getName(), template.className()); + } + + @Test + public void testJobTemplateHasArguments() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.arguments()); + assertEquals(12, template.arguments().size()); // 6 flags * 2 (flag + value) + + // Verify all expected arguments are present + assertTrue(template.arguments().contains("--catalog")); + assertTrue(template.arguments().contains("{{catalog_name}}")); + assertTrue(template.arguments().contains("--table")); + assertTrue(template.arguments().contains("{{table_identifier}}")); + assertTrue(template.arguments().contains("--older-than")); + assertTrue(template.arguments().contains("{{older_than}}")); + assertTrue(template.arguments().contains("--retain-last")); + assertTrue(template.arguments().contains("{{retain_last}}")); + assertTrue(template.arguments().contains("--stream-results")); + assertTrue(template.arguments().contains("{{stream_results}}")); + assertTrue(template.arguments().contains("--spark-conf")); + assertTrue(template.arguments().contains("{{spark_conf}}")); + } + + @Test + public void testJobTemplateHasSparkConfigs() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + Map<String, String> configs = template.configs(); + assertNotNull(configs); + assertFalse(configs.isEmpty()); + + // Verify Spark runtime configs + assertTrue(configs.containsKey("spark.master")); + assertTrue(configs.containsKey("spark.executor.instances")); + assertTrue(configs.containsKey("spark.executor.cores")); + assertTrue(configs.containsKey("spark.executor.memory")); + assertTrue(configs.containsKey("spark.driver.memory")); + + // Verify Iceberg catalog configs + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.type")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.uri")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.warehouse")); + + // Verify Iceberg extensions + assertTrue(configs.containsKey("spark.sql.extensions")); + assertEquals( + "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions", + configs.get("spark.sql.extensions")); + } + + @Test + public void testJobTemplateHasVersion() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + Map<String, String> customFields = template.customFields(); + assertNotNull(customFields); + assertTrue(customFields.containsKey(JobTemplateProvider.PROPERTY_VERSION_KEY)); + + String version = customFields.get(JobTemplateProvider.PROPERTY_VERSION_KEY); + assertEquals("v1", version); + assertTrue(version.matches(JobTemplateProvider.VERSION_VALUE_PATTERN)); + } + + @Test + public void testJobTemplateNameMatchesBuiltInPattern() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertTrue(template.name().matches(JobTemplateProvider.BUILTIN_NAME_PATTERN)); + assertTrue(template.name().startsWith(JobTemplateProvider.BUILTIN_NAME_PREFIX)); + } + + // Test parseArguments method + + @Test + public void testParseArgumentsWithAllRequired() { + String[] args = {"--catalog", "iceberg_prod", "--table", "db.sample"}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(2, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + } + + @Test + public void testParseArgumentsWithOptional() { + String[] args = { + "--catalog", "iceberg_prod", + "--table", "db.sample", + "--older-than", "2024-01-01 00:00:00", + "--retain-last", "5" + }; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(4, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertEquals("2024-01-01 00:00:00", result.get("older-than")); + assertEquals("5", result.get("retain-last")); + } + + @Test + public void testParseArgumentsWithEmptyValues() { + String[] args = {"--catalog", "iceberg_prod", "--table", "db.sample", "--older-than", ""}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + // Empty values should be ignored + assertEquals(2, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertFalse(result.containsKey("older-than")); + } + + @Test + public void testParseArgumentsWithMissingValues() { + String[] args = {"--catalog", "iceberg_prod", "--table"}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + // Only catalog should be parsed, table has no value + assertEquals(1, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertFalse(result.containsKey("table")); + } + + @Test + public void testParseArgumentsWithAllOptions() { + String[] args = { + "--catalog", "iceberg_prod", + "--table", "db.sample", + "--older-than", "2024-06-01 00:00:00", + "--retain-last", "3", + "--stream-results", "true", + "--spark-conf", "{\"spark.executor.memory\":\"4g\"}" + }; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(6, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertEquals("2024-06-01 00:00:00", result.get("older-than")); + assertEquals("3", result.get("retain-last")); + assertEquals("true", result.get("stream-results")); + assertEquals("{\"spark.executor.memory\":\"4g\"}", result.get("spark-conf")); + } + + @Test + public void testParseArgumentsOrderIndependent() { + String[] args1 = {"--catalog", "cat1", "--table", "tbl1", "--retain-last", "5"}; + String[] args2 = {"--retain-last", "5", "--table", "tbl1", "--catalog", "cat1"}; + + Map<String, String> result1 = IcebergExpireSnapshotsJob.parseArguments(args1); + Map<String, String> result2 = IcebergExpireSnapshotsJob.parseArguments(args2); + + assertEquals(result1, result2); + } + + // Test buildProcedureCall method + + @Test + public void testBuildProcedureCallMinimal() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, null, null); + + assertEquals("CALL iceberg_prod.system.expire_snapshots(table => 'db.sample')", sql); + } + + @Test + public void testBuildProcedureCallWithOlderThan() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_prod", "db.sample", "2024-01-01 00:00:00", null, null); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', " + + "older_than => TIMESTAMP '2024-01-01 00:00:00')", + sql); Review Comment: Done - `escapeSqlIdentifier()` was updated to return the backtick-quoted form, and this expected SQL / assertion has been updated accordingly (including the catalog-name injection cases, which now assert the quoted identifier). ########## maintenance/jobs/src/test/java/org/apache/gravitino/maintenance/jobs/iceberg/TestIcebergExpireSnapshotsJob.java: ########## @@ -0,0 +1,506 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.maintenance.jobs.iceberg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Map; +import org.apache.gravitino.job.JobTemplateProvider; +import org.apache.gravitino.job.SparkJobTemplate; +import org.junit.jupiter.api.Test; + +public class TestIcebergExpireSnapshotsJob { + + @Test + public void testJobTemplateHasCorrectName() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template); + assertEquals("builtin-iceberg-expire-snapshots", template.name()); + } + + @Test + public void testJobTemplateHasComment() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.comment()); + assertFalse(template.comment().trim().isEmpty()); + assertTrue(template.comment().contains("Iceberg")); + } + + @Test + public void testJobTemplateHasExecutable() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.executable()); + assertFalse(template.executable().trim().isEmpty()); + } + + @Test + public void testJobTemplateHasMainClass() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.className()); + assertEquals(IcebergExpireSnapshotsJob.class.getName(), template.className()); + } + + @Test + public void testJobTemplateHasArguments() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.arguments()); + assertEquals(12, template.arguments().size()); // 6 flags * 2 (flag + value) + + // Verify all expected arguments are present + assertTrue(template.arguments().contains("--catalog")); + assertTrue(template.arguments().contains("{{catalog_name}}")); + assertTrue(template.arguments().contains("--table")); + assertTrue(template.arguments().contains("{{table_identifier}}")); + assertTrue(template.arguments().contains("--older-than")); + assertTrue(template.arguments().contains("{{older_than}}")); + assertTrue(template.arguments().contains("--retain-last")); + assertTrue(template.arguments().contains("{{retain_last}}")); + assertTrue(template.arguments().contains("--stream-results")); + assertTrue(template.arguments().contains("{{stream_results}}")); + assertTrue(template.arguments().contains("--spark-conf")); + assertTrue(template.arguments().contains("{{spark_conf}}")); + } + + @Test + public void testJobTemplateHasSparkConfigs() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + Map<String, String> configs = template.configs(); + assertNotNull(configs); + assertFalse(configs.isEmpty()); + + // Verify Spark runtime configs + assertTrue(configs.containsKey("spark.master")); + assertTrue(configs.containsKey("spark.executor.instances")); + assertTrue(configs.containsKey("spark.executor.cores")); + assertTrue(configs.containsKey("spark.executor.memory")); + assertTrue(configs.containsKey("spark.driver.memory")); + + // Verify Iceberg catalog configs + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.type")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.uri")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.warehouse")); + + // Verify Iceberg extensions + assertTrue(configs.containsKey("spark.sql.extensions")); + assertEquals( + "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions", + configs.get("spark.sql.extensions")); + } + + @Test + public void testJobTemplateHasVersion() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + Map<String, String> customFields = template.customFields(); + assertNotNull(customFields); + assertTrue(customFields.containsKey(JobTemplateProvider.PROPERTY_VERSION_KEY)); + + String version = customFields.get(JobTemplateProvider.PROPERTY_VERSION_KEY); + assertEquals("v1", version); + assertTrue(version.matches(JobTemplateProvider.VERSION_VALUE_PATTERN)); + } + + @Test + public void testJobTemplateNameMatchesBuiltInPattern() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertTrue(template.name().matches(JobTemplateProvider.BUILTIN_NAME_PATTERN)); + assertTrue(template.name().startsWith(JobTemplateProvider.BUILTIN_NAME_PREFIX)); + } + + // Test parseArguments method + + @Test + public void testParseArgumentsWithAllRequired() { + String[] args = {"--catalog", "iceberg_prod", "--table", "db.sample"}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(2, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + } + + @Test + public void testParseArgumentsWithOptional() { + String[] args = { + "--catalog", "iceberg_prod", + "--table", "db.sample", + "--older-than", "2024-01-01 00:00:00", + "--retain-last", "5" + }; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(4, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertEquals("2024-01-01 00:00:00", result.get("older-than")); + assertEquals("5", result.get("retain-last")); + } + + @Test + public void testParseArgumentsWithEmptyValues() { + String[] args = {"--catalog", "iceberg_prod", "--table", "db.sample", "--older-than", ""}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + // Empty values should be ignored + assertEquals(2, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertFalse(result.containsKey("older-than")); + } + + @Test + public void testParseArgumentsWithMissingValues() { + String[] args = {"--catalog", "iceberg_prod", "--table"}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + // Only catalog should be parsed, table has no value + assertEquals(1, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertFalse(result.containsKey("table")); + } + + @Test + public void testParseArgumentsWithAllOptions() { + String[] args = { + "--catalog", "iceberg_prod", + "--table", "db.sample", + "--older-than", "2024-06-01 00:00:00", + "--retain-last", "3", + "--stream-results", "true", + "--spark-conf", "{\"spark.executor.memory\":\"4g\"}" + }; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(6, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertEquals("2024-06-01 00:00:00", result.get("older-than")); + assertEquals("3", result.get("retain-last")); + assertEquals("true", result.get("stream-results")); + assertEquals("{\"spark.executor.memory\":\"4g\"}", result.get("spark-conf")); + } + + @Test + public void testParseArgumentsOrderIndependent() { + String[] args1 = {"--catalog", "cat1", "--table", "tbl1", "--retain-last", "5"}; + String[] args2 = {"--retain-last", "5", "--table", "tbl1", "--catalog", "cat1"}; + + Map<String, String> result1 = IcebergExpireSnapshotsJob.parseArguments(args1); + Map<String, String> result2 = IcebergExpireSnapshotsJob.parseArguments(args2); + + assertEquals(result1, result2); + } + + // Test buildProcedureCall method + + @Test + public void testBuildProcedureCallMinimal() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, null, null); + + assertEquals("CALL iceberg_prod.system.expire_snapshots(table => 'db.sample')", sql); + } + + @Test + public void testBuildProcedureCallWithOlderThan() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_prod", "db.sample", "2024-01-01 00:00:00", null, null); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', " + + "older_than => TIMESTAMP '2024-01-01 00:00:00')", + sql); + } + + @Test + public void testBuildProcedureCallWithRetainLast() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, "5", null); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', retain_last => 5)", sql); Review Comment: Done - `escapeSqlIdentifier()` was updated to return the backtick-quoted form, and this expected SQL / assertion has been updated accordingly (including the catalog-name injection cases, which now assert the quoted identifier). ########## maintenance/jobs/src/test/java/org/apache/gravitino/maintenance/jobs/iceberg/TestIcebergExpireSnapshotsJob.java: ########## @@ -0,0 +1,506 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.maintenance.jobs.iceberg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Map; +import org.apache.gravitino.job.JobTemplateProvider; +import org.apache.gravitino.job.SparkJobTemplate; +import org.junit.jupiter.api.Test; + +public class TestIcebergExpireSnapshotsJob { + + @Test + public void testJobTemplateHasCorrectName() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template); + assertEquals("builtin-iceberg-expire-snapshots", template.name()); + } + + @Test + public void testJobTemplateHasComment() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.comment()); + assertFalse(template.comment().trim().isEmpty()); + assertTrue(template.comment().contains("Iceberg")); + } + + @Test + public void testJobTemplateHasExecutable() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.executable()); + assertFalse(template.executable().trim().isEmpty()); + } + + @Test + public void testJobTemplateHasMainClass() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.className()); + assertEquals(IcebergExpireSnapshotsJob.class.getName(), template.className()); + } + + @Test + public void testJobTemplateHasArguments() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.arguments()); + assertEquals(12, template.arguments().size()); // 6 flags * 2 (flag + value) + + // Verify all expected arguments are present + assertTrue(template.arguments().contains("--catalog")); + assertTrue(template.arguments().contains("{{catalog_name}}")); + assertTrue(template.arguments().contains("--table")); + assertTrue(template.arguments().contains("{{table_identifier}}")); + assertTrue(template.arguments().contains("--older-than")); + assertTrue(template.arguments().contains("{{older_than}}")); + assertTrue(template.arguments().contains("--retain-last")); + assertTrue(template.arguments().contains("{{retain_last}}")); + assertTrue(template.arguments().contains("--stream-results")); + assertTrue(template.arguments().contains("{{stream_results}}")); + assertTrue(template.arguments().contains("--spark-conf")); + assertTrue(template.arguments().contains("{{spark_conf}}")); + } + + @Test + public void testJobTemplateHasSparkConfigs() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + Map<String, String> configs = template.configs(); + assertNotNull(configs); + assertFalse(configs.isEmpty()); + + // Verify Spark runtime configs + assertTrue(configs.containsKey("spark.master")); + assertTrue(configs.containsKey("spark.executor.instances")); + assertTrue(configs.containsKey("spark.executor.cores")); + assertTrue(configs.containsKey("spark.executor.memory")); + assertTrue(configs.containsKey("spark.driver.memory")); + + // Verify Iceberg catalog configs + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.type")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.uri")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.warehouse")); + + // Verify Iceberg extensions + assertTrue(configs.containsKey("spark.sql.extensions")); + assertEquals( + "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions", + configs.get("spark.sql.extensions")); + } + + @Test + public void testJobTemplateHasVersion() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + Map<String, String> customFields = template.customFields(); + assertNotNull(customFields); + assertTrue(customFields.containsKey(JobTemplateProvider.PROPERTY_VERSION_KEY)); + + String version = customFields.get(JobTemplateProvider.PROPERTY_VERSION_KEY); + assertEquals("v1", version); + assertTrue(version.matches(JobTemplateProvider.VERSION_VALUE_PATTERN)); + } + + @Test + public void testJobTemplateNameMatchesBuiltInPattern() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertTrue(template.name().matches(JobTemplateProvider.BUILTIN_NAME_PATTERN)); + assertTrue(template.name().startsWith(JobTemplateProvider.BUILTIN_NAME_PREFIX)); + } + + // Test parseArguments method + + @Test + public void testParseArgumentsWithAllRequired() { + String[] args = {"--catalog", "iceberg_prod", "--table", "db.sample"}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(2, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + } + + @Test + public void testParseArgumentsWithOptional() { + String[] args = { + "--catalog", "iceberg_prod", + "--table", "db.sample", + "--older-than", "2024-01-01 00:00:00", + "--retain-last", "5" + }; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(4, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertEquals("2024-01-01 00:00:00", result.get("older-than")); + assertEquals("5", result.get("retain-last")); + } + + @Test + public void testParseArgumentsWithEmptyValues() { + String[] args = {"--catalog", "iceberg_prod", "--table", "db.sample", "--older-than", ""}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + // Empty values should be ignored + assertEquals(2, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertFalse(result.containsKey("older-than")); + } + + @Test + public void testParseArgumentsWithMissingValues() { + String[] args = {"--catalog", "iceberg_prod", "--table"}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + // Only catalog should be parsed, table has no value + assertEquals(1, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertFalse(result.containsKey("table")); + } + + @Test + public void testParseArgumentsWithAllOptions() { + String[] args = { + "--catalog", "iceberg_prod", + "--table", "db.sample", + "--older-than", "2024-06-01 00:00:00", + "--retain-last", "3", + "--stream-results", "true", + "--spark-conf", "{\"spark.executor.memory\":\"4g\"}" + }; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(6, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertEquals("2024-06-01 00:00:00", result.get("older-than")); + assertEquals("3", result.get("retain-last")); + assertEquals("true", result.get("stream-results")); + assertEquals("{\"spark.executor.memory\":\"4g\"}", result.get("spark-conf")); + } + + @Test + public void testParseArgumentsOrderIndependent() { + String[] args1 = {"--catalog", "cat1", "--table", "tbl1", "--retain-last", "5"}; + String[] args2 = {"--retain-last", "5", "--table", "tbl1", "--catalog", "cat1"}; + + Map<String, String> result1 = IcebergExpireSnapshotsJob.parseArguments(args1); + Map<String, String> result2 = IcebergExpireSnapshotsJob.parseArguments(args2); + + assertEquals(result1, result2); + } + + // Test buildProcedureCall method + + @Test + public void testBuildProcedureCallMinimal() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, null, null); + + assertEquals("CALL iceberg_prod.system.expire_snapshots(table => 'db.sample')", sql); + } + + @Test + public void testBuildProcedureCallWithOlderThan() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_prod", "db.sample", "2024-01-01 00:00:00", null, null); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', " + + "older_than => TIMESTAMP '2024-01-01 00:00:00')", + sql); + } + + @Test + public void testBuildProcedureCallWithRetainLast() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, "5", null); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', retain_last => 5)", sql); + } + + @Test + public void testBuildProcedureCallWithStreamResults() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_prod", "db.sample", null, null, "true"); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', " + + "stream_results => true)", + sql); Review Comment: Done - `escapeSqlIdentifier()` was updated to return the backtick-quoted form, and this expected SQL / assertion has been updated accordingly (including the catalog-name injection cases, which now assert the quoted identifier). ########## maintenance/jobs/src/test/java/org/apache/gravitino/maintenance/jobs/iceberg/TestIcebergExpireSnapshotsJob.java: ########## @@ -0,0 +1,506 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.maintenance.jobs.iceberg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Map; +import org.apache.gravitino.job.JobTemplateProvider; +import org.apache.gravitino.job.SparkJobTemplate; +import org.junit.jupiter.api.Test; + +public class TestIcebergExpireSnapshotsJob { + + @Test + public void testJobTemplateHasCorrectName() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template); + assertEquals("builtin-iceberg-expire-snapshots", template.name()); + } + + @Test + public void testJobTemplateHasComment() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.comment()); + assertFalse(template.comment().trim().isEmpty()); + assertTrue(template.comment().contains("Iceberg")); + } + + @Test + public void testJobTemplateHasExecutable() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.executable()); + assertFalse(template.executable().trim().isEmpty()); + } + + @Test + public void testJobTemplateHasMainClass() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.className()); + assertEquals(IcebergExpireSnapshotsJob.class.getName(), template.className()); + } + + @Test + public void testJobTemplateHasArguments() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.arguments()); + assertEquals(12, template.arguments().size()); // 6 flags * 2 (flag + value) + + // Verify all expected arguments are present + assertTrue(template.arguments().contains("--catalog")); + assertTrue(template.arguments().contains("{{catalog_name}}")); + assertTrue(template.arguments().contains("--table")); + assertTrue(template.arguments().contains("{{table_identifier}}")); + assertTrue(template.arguments().contains("--older-than")); + assertTrue(template.arguments().contains("{{older_than}}")); + assertTrue(template.arguments().contains("--retain-last")); + assertTrue(template.arguments().contains("{{retain_last}}")); + assertTrue(template.arguments().contains("--stream-results")); + assertTrue(template.arguments().contains("{{stream_results}}")); + assertTrue(template.arguments().contains("--spark-conf")); + assertTrue(template.arguments().contains("{{spark_conf}}")); + } + + @Test + public void testJobTemplateHasSparkConfigs() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + Map<String, String> configs = template.configs(); + assertNotNull(configs); + assertFalse(configs.isEmpty()); + + // Verify Spark runtime configs + assertTrue(configs.containsKey("spark.master")); + assertTrue(configs.containsKey("spark.executor.instances")); + assertTrue(configs.containsKey("spark.executor.cores")); + assertTrue(configs.containsKey("spark.executor.memory")); + assertTrue(configs.containsKey("spark.driver.memory")); + + // Verify Iceberg catalog configs + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.type")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.uri")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.warehouse")); + + // Verify Iceberg extensions + assertTrue(configs.containsKey("spark.sql.extensions")); + assertEquals( + "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions", + configs.get("spark.sql.extensions")); + } + + @Test + public void testJobTemplateHasVersion() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + Map<String, String> customFields = template.customFields(); + assertNotNull(customFields); + assertTrue(customFields.containsKey(JobTemplateProvider.PROPERTY_VERSION_KEY)); + + String version = customFields.get(JobTemplateProvider.PROPERTY_VERSION_KEY); + assertEquals("v1", version); + assertTrue(version.matches(JobTemplateProvider.VERSION_VALUE_PATTERN)); + } + + @Test + public void testJobTemplateNameMatchesBuiltInPattern() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertTrue(template.name().matches(JobTemplateProvider.BUILTIN_NAME_PATTERN)); + assertTrue(template.name().startsWith(JobTemplateProvider.BUILTIN_NAME_PREFIX)); + } + + // Test parseArguments method + + @Test + public void testParseArgumentsWithAllRequired() { + String[] args = {"--catalog", "iceberg_prod", "--table", "db.sample"}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(2, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + } + + @Test + public void testParseArgumentsWithOptional() { + String[] args = { + "--catalog", "iceberg_prod", + "--table", "db.sample", + "--older-than", "2024-01-01 00:00:00", + "--retain-last", "5" + }; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(4, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertEquals("2024-01-01 00:00:00", result.get("older-than")); + assertEquals("5", result.get("retain-last")); + } + + @Test + public void testParseArgumentsWithEmptyValues() { + String[] args = {"--catalog", "iceberg_prod", "--table", "db.sample", "--older-than", ""}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + // Empty values should be ignored + assertEquals(2, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertFalse(result.containsKey("older-than")); + } + + @Test + public void testParseArgumentsWithMissingValues() { + String[] args = {"--catalog", "iceberg_prod", "--table"}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + // Only catalog should be parsed, table has no value + assertEquals(1, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertFalse(result.containsKey("table")); + } + + @Test + public void testParseArgumentsWithAllOptions() { + String[] args = { + "--catalog", "iceberg_prod", + "--table", "db.sample", + "--older-than", "2024-06-01 00:00:00", + "--retain-last", "3", + "--stream-results", "true", + "--spark-conf", "{\"spark.executor.memory\":\"4g\"}" + }; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(6, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertEquals("2024-06-01 00:00:00", result.get("older-than")); + assertEquals("3", result.get("retain-last")); + assertEquals("true", result.get("stream-results")); + assertEquals("{\"spark.executor.memory\":\"4g\"}", result.get("spark-conf")); + } + + @Test + public void testParseArgumentsOrderIndependent() { + String[] args1 = {"--catalog", "cat1", "--table", "tbl1", "--retain-last", "5"}; + String[] args2 = {"--retain-last", "5", "--table", "tbl1", "--catalog", "cat1"}; + + Map<String, String> result1 = IcebergExpireSnapshotsJob.parseArguments(args1); + Map<String, String> result2 = IcebergExpireSnapshotsJob.parseArguments(args2); + + assertEquals(result1, result2); + } + + // Test buildProcedureCall method + + @Test + public void testBuildProcedureCallMinimal() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, null, null); + + assertEquals("CALL iceberg_prod.system.expire_snapshots(table => 'db.sample')", sql); + } + + @Test + public void testBuildProcedureCallWithOlderThan() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_prod", "db.sample", "2024-01-01 00:00:00", null, null); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', " + + "older_than => TIMESTAMP '2024-01-01 00:00:00')", + sql); + } + + @Test + public void testBuildProcedureCallWithRetainLast() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, "5", null); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', retain_last => 5)", sql); + } + + @Test + public void testBuildProcedureCallWithStreamResults() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_prod", "db.sample", null, null, "true"); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', " + + "stream_results => true)", + sql); + } + + @Test + public void testBuildProcedureCallWithAllParameters() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_prod", "db.sample", "2024-01-01 00:00:00", "3", "true"); + + assertTrue(sql.startsWith("CALL iceberg_prod.system.expire_snapshots(")); + assertTrue(sql.contains("table => 'db.sample'")); + assertTrue(sql.contains("older_than => TIMESTAMP '2024-01-01 00:00:00'")); + assertTrue(sql.contains("retain_last => 3")); + assertTrue(sql.contains("stream_results => true")); + assertTrue(sql.endsWith(")")); + } + + @Test + public void testBuildProcedureCallWithEmptyOlderThan() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", "", null, null); + + assertEquals("CALL iceberg_prod.system.expire_snapshots(table => 'db.sample')", sql); + } + + @Test + public void testBuildProcedureCallWithEmptyRetainLast() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, "", null); + + assertEquals("CALL iceberg_prod.system.expire_snapshots(table => 'db.sample')", sql); Review Comment: Done - `escapeSqlIdentifier()` was updated to return the backtick-quoted form, and this expected SQL / assertion has been updated accordingly (including the catalog-name injection cases, which now assert the quoted identifier). ########## maintenance/jobs/src/test/java/org/apache/gravitino/maintenance/jobs/iceberg/TestIcebergExpireSnapshotsJob.java: ########## @@ -0,0 +1,506 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.maintenance.jobs.iceberg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.Map; +import org.apache.gravitino.job.JobTemplateProvider; +import org.apache.gravitino.job.SparkJobTemplate; +import org.junit.jupiter.api.Test; + +public class TestIcebergExpireSnapshotsJob { + + @Test + public void testJobTemplateHasCorrectName() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template); + assertEquals("builtin-iceberg-expire-snapshots", template.name()); + } + + @Test + public void testJobTemplateHasComment() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.comment()); + assertFalse(template.comment().trim().isEmpty()); + assertTrue(template.comment().contains("Iceberg")); + } + + @Test + public void testJobTemplateHasExecutable() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.executable()); + assertFalse(template.executable().trim().isEmpty()); + } + + @Test + public void testJobTemplateHasMainClass() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.className()); + assertEquals(IcebergExpireSnapshotsJob.class.getName(), template.className()); + } + + @Test + public void testJobTemplateHasArguments() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertNotNull(template.arguments()); + assertEquals(12, template.arguments().size()); // 6 flags * 2 (flag + value) + + // Verify all expected arguments are present + assertTrue(template.arguments().contains("--catalog")); + assertTrue(template.arguments().contains("{{catalog_name}}")); + assertTrue(template.arguments().contains("--table")); + assertTrue(template.arguments().contains("{{table_identifier}}")); + assertTrue(template.arguments().contains("--older-than")); + assertTrue(template.arguments().contains("{{older_than}}")); + assertTrue(template.arguments().contains("--retain-last")); + assertTrue(template.arguments().contains("{{retain_last}}")); + assertTrue(template.arguments().contains("--stream-results")); + assertTrue(template.arguments().contains("{{stream_results}}")); + assertTrue(template.arguments().contains("--spark-conf")); + assertTrue(template.arguments().contains("{{spark_conf}}")); + } + + @Test + public void testJobTemplateHasSparkConfigs() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + Map<String, String> configs = template.configs(); + assertNotNull(configs); + assertFalse(configs.isEmpty()); + + // Verify Spark runtime configs + assertTrue(configs.containsKey("spark.master")); + assertTrue(configs.containsKey("spark.executor.instances")); + assertTrue(configs.containsKey("spark.executor.cores")); + assertTrue(configs.containsKey("spark.executor.memory")); + assertTrue(configs.containsKey("spark.driver.memory")); + + // Verify Iceberg catalog configs + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.type")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.uri")); + assertTrue(configs.containsKey("spark.sql.catalog.{{catalog_name}}.warehouse")); + + // Verify Iceberg extensions + assertTrue(configs.containsKey("spark.sql.extensions")); + assertEquals( + "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions", + configs.get("spark.sql.extensions")); + } + + @Test + public void testJobTemplateHasVersion() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + Map<String, String> customFields = template.customFields(); + assertNotNull(customFields); + assertTrue(customFields.containsKey(JobTemplateProvider.PROPERTY_VERSION_KEY)); + + String version = customFields.get(JobTemplateProvider.PROPERTY_VERSION_KEY); + assertEquals("v1", version); + assertTrue(version.matches(JobTemplateProvider.VERSION_VALUE_PATTERN)); + } + + @Test + public void testJobTemplateNameMatchesBuiltInPattern() { + IcebergExpireSnapshotsJob job = new IcebergExpireSnapshotsJob(); + SparkJobTemplate template = job.jobTemplate(); + + assertTrue(template.name().matches(JobTemplateProvider.BUILTIN_NAME_PATTERN)); + assertTrue(template.name().startsWith(JobTemplateProvider.BUILTIN_NAME_PREFIX)); + } + + // Test parseArguments method + + @Test + public void testParseArgumentsWithAllRequired() { + String[] args = {"--catalog", "iceberg_prod", "--table", "db.sample"}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(2, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + } + + @Test + public void testParseArgumentsWithOptional() { + String[] args = { + "--catalog", "iceberg_prod", + "--table", "db.sample", + "--older-than", "2024-01-01 00:00:00", + "--retain-last", "5" + }; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(4, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertEquals("2024-01-01 00:00:00", result.get("older-than")); + assertEquals("5", result.get("retain-last")); + } + + @Test + public void testParseArgumentsWithEmptyValues() { + String[] args = {"--catalog", "iceberg_prod", "--table", "db.sample", "--older-than", ""}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + // Empty values should be ignored + assertEquals(2, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertFalse(result.containsKey("older-than")); + } + + @Test + public void testParseArgumentsWithMissingValues() { + String[] args = {"--catalog", "iceberg_prod", "--table"}; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + // Only catalog should be parsed, table has no value + assertEquals(1, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertFalse(result.containsKey("table")); + } + + @Test + public void testParseArgumentsWithAllOptions() { + String[] args = { + "--catalog", "iceberg_prod", + "--table", "db.sample", + "--older-than", "2024-06-01 00:00:00", + "--retain-last", "3", + "--stream-results", "true", + "--spark-conf", "{\"spark.executor.memory\":\"4g\"}" + }; + Map<String, String> result = IcebergExpireSnapshotsJob.parseArguments(args); + + assertEquals(6, result.size()); + assertEquals("iceberg_prod", result.get("catalog")); + assertEquals("db.sample", result.get("table")); + assertEquals("2024-06-01 00:00:00", result.get("older-than")); + assertEquals("3", result.get("retain-last")); + assertEquals("true", result.get("stream-results")); + assertEquals("{\"spark.executor.memory\":\"4g\"}", result.get("spark-conf")); + } + + @Test + public void testParseArgumentsOrderIndependent() { + String[] args1 = {"--catalog", "cat1", "--table", "tbl1", "--retain-last", "5"}; + String[] args2 = {"--retain-last", "5", "--table", "tbl1", "--catalog", "cat1"}; + + Map<String, String> result1 = IcebergExpireSnapshotsJob.parseArguments(args1); + Map<String, String> result2 = IcebergExpireSnapshotsJob.parseArguments(args2); + + assertEquals(result1, result2); + } + + // Test buildProcedureCall method + + @Test + public void testBuildProcedureCallMinimal() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, null, null); + + assertEquals("CALL iceberg_prod.system.expire_snapshots(table => 'db.sample')", sql); + } + + @Test + public void testBuildProcedureCallWithOlderThan() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_prod", "db.sample", "2024-01-01 00:00:00", null, null); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', " + + "older_than => TIMESTAMP '2024-01-01 00:00:00')", + sql); + } + + @Test + public void testBuildProcedureCallWithRetainLast() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, "5", null); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', retain_last => 5)", sql); + } + + @Test + public void testBuildProcedureCallWithStreamResults() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_prod", "db.sample", null, null, "true"); + + assertEquals( + "CALL iceberg_prod.system.expire_snapshots(table => 'db.sample', " + + "stream_results => true)", + sql); + } + + @Test + public void testBuildProcedureCallWithAllParameters() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_prod", "db.sample", "2024-01-01 00:00:00", "3", "true"); + + assertTrue(sql.startsWith("CALL iceberg_prod.system.expire_snapshots(")); + assertTrue(sql.contains("table => 'db.sample'")); + assertTrue(sql.contains("older_than => TIMESTAMP '2024-01-01 00:00:00'")); + assertTrue(sql.contains("retain_last => 3")); + assertTrue(sql.contains("stream_results => true")); + assertTrue(sql.endsWith(")")); + } + + @Test + public void testBuildProcedureCallWithEmptyOlderThan() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", "", null, null); + + assertEquals("CALL iceberg_prod.system.expire_snapshots(table => 'db.sample')", sql); + } + + @Test + public void testBuildProcedureCallWithEmptyRetainLast() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall("iceberg_prod", "db.sample", null, "", null); + + assertEquals("CALL iceberg_prod.system.expire_snapshots(table => 'db.sample')", sql); + } + + // Test SQL escaping + + @Test + public void testEscapeSqlString() { + // Test basic escaping of single quotes + assertEquals("O''Brien", IcebergExpireSnapshotsJob.escapeSqlString("O'Brien")); + assertEquals( + "test''with''quotes", IcebergExpireSnapshotsJob.escapeSqlString("test'with'quotes")); + + // Test strings without quotes remain unchanged + assertEquals("normal_string", IcebergExpireSnapshotsJob.escapeSqlString("normal_string")); + + // Test null and empty + assertEquals(null, IcebergExpireSnapshotsJob.escapeSqlString(null)); + assertEquals("", IcebergExpireSnapshotsJob.escapeSqlString("")); + } + + @Test + public void testEscapeSqlIdentifier() { + // Test basic escaping of backticks + assertEquals("catalog``name", IcebergExpireSnapshotsJob.escapeSqlIdentifier("catalog`name")); + + // Test strings without backticks remain unchanged + assertEquals("normal_catalog", IcebergExpireSnapshotsJob.escapeSqlIdentifier("normal_catalog")); + + // Test null + assertEquals(null, IcebergExpireSnapshotsJob.escapeSqlIdentifier(null)); + } + + @Test + public void testBuildProcedureCallWithSqlInjectionAttempt() { + // Test SQL injection attempt in table name + String maliciousTable = "db.table' OR '1'='1"; + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_catalog", maliciousTable, null, null, null); + + // Verify single quotes are escaped (becomes '') + assertTrue(sql.contains("db.table'' OR ''1''=''1")); + assertFalse(sql.contains("' OR '1'='1")); + + // Test SQL injection attempt in older-than + String maliciousOlderThan = "2024-01-01' OR '1'='1"; + sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "iceberg_catalog", "db.table", maliciousOlderThan, null, null); + + assertTrue(sql.contains("2024-01-01'' OR ''1''=''1")); + + // Test SQL injection attempt in catalog name + String maliciousCatalog = "catalog`; DROP TABLE users; --"; + sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + maliciousCatalog, "db.table", null, null, null); + + // Verify backticks are escaped + assertTrue(sql.contains("catalog``; DROP TABLE users; --")); + } + + @Test + public void testBuildProcedureCallEscapesTableIdentifier() { + String sql = + IcebergExpireSnapshotsJob.buildProcedureCall( + "cat'alog", "db'.table", "2024-01-01' DROP TABLE", null, null); + + // Catalog name uses backtick escaping (but no backticks here, so unchanged) + assertTrue(sql.contains("cat'alog")); Review Comment: Done - `escapeSqlIdentifier()` was updated to return the backtick-quoted form, and this expected SQL / assertion has been updated accordingly (including the catalog-name injection cases, which now assert the quoted identifier). -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
