diff --git a/.github/scripts/configure-runners.sh b/.github/scripts/configure-runners.sh index a51b9aaf7e..1842e31e9c 100755 --- a/.github/scripts/configure-runners.sh +++ b/.github/scripts/configure-runners.sh @@ -18,7 +18,7 @@ # Defaults NAME_SUFFIX="it" -SIZE=3 +SIZE=5 BASE_NAME="gitactions-runner" REPO_NAME="DataflowTemplates" REPO_OWNER="GoogleCloudPlatform" diff --git a/.github/scripts/startup-script.sh b/.github/scripts/startup-script.sh index fa782dae38..c359adfa50 100644 --- a/.github/scripts/startup-script.sh +++ b/.github/scripts/startup-script.sh @@ -27,6 +27,9 @@ sudo add-apt-repository ppa:git-core/ppa -y sudo apt update sudo apt install git -y +# update Java version to 17 +sudo apt install openjdk-17-jdk-headless -y + # install jq sudo apt install jq -y diff --git a/contributor-docs/cicd.md b/contributor-docs/cicd.md index 736db3c228..191d2dd5ef 100644 --- a/contributor-docs/cicd.md +++ b/contributor-docs/cicd.md @@ -92,5 +92,15 @@ To run the configuration script: -S perf \ -s 1 ``` + + * For Release Runner + ``` + ./configure-runners.sh \ + -p cloud-teleport-testing \ + -a 269744978479-compute@developer.gserviceaccount.com \ + -t $GITACTIONS_TOKEN \ + -S release \ + -s 1 + ``` **Note**: To see optional configurable parameters, run `./configure-runners.sh -h` diff --git a/v2/googlecloud-to-neo4j/src/main/java/com/google/cloud/teleport/v2/neo4j/templates/GoogleCloudToNeo4j.java b/v2/googlecloud-to-neo4j/src/main/java/com/google/cloud/teleport/v2/neo4j/templates/GoogleCloudToNeo4j.java index cde511e47f..89e6c04387 100644 --- a/v2/googlecloud-to-neo4j/src/main/java/com/google/cloud/teleport/v2/neo4j/templates/GoogleCloudToNeo4j.java +++ b/v2/googlecloud-to-neo4j/src/main/java/com/google/cloud/teleport/v2/neo4j/templates/GoogleCloudToNeo4j.java @@ -219,7 +219,7 @@ public static void main(String[] args) { PipelineOptionsFactory.fromArgs(args).withValidation().as(Neo4jFlexTemplateOptions.class); // Allow users to supply their own list of disabled algorithms if necessary - if (StringUtils.isNotBlank(options.getDisabledAlgorithms())) { + if (StringUtils.isBlank(options.getDisabledAlgorithms())) { options.setDisabledAlgorithms( "SSLv3, RC4, DES, MD5withRSA, DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon," + " NULL"); diff --git a/v2/sourcedb-to-spanner/src/test/java/com/google/cloud/teleport/v2/templates/MySQLDDLIT.java b/v2/sourcedb-to-spanner/src/test/java/com/google/cloud/teleport/v2/templates/MySQLDDLIT.java new file mode 100644 index 0000000000..ce7fad9943 --- /dev/null +++ b/v2/sourcedb-to-spanner/src/test/java/com/google/cloud/teleport/v2/templates/MySQLDDLIT.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 Google LLC + * + * Licensed 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 com.google.cloud.teleport.v2.templates; + +import com.google.cloud.spanner.Struct; +import com.google.cloud.teleport.metadata.SkipDirectRunnerTest; +import com.google.cloud.teleport.metadata.TemplateIntegrationTest; +import com.google.common.collect.ImmutableList; +import java.util.List; +import java.util.Map; +import org.apache.beam.it.common.PipelineLauncher; +import org.apache.beam.it.common.PipelineOperator; +import org.apache.beam.it.common.utils.ResourceManagerUtils; +import org.apache.beam.it.gcp.spanner.SpannerResourceManager; +import org.apache.beam.it.gcp.spanner.matchers.SpannerAsserts; +import org.apache.beam.it.jdbc.MySQLResourceManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * An integration test for {@link SourceDbToSpanner} Flex template which tests a migration with DDL + * changes to schema. Changes include Index changes, Primary key transformations and Generated + * columns migration. + */ +@Category({TemplateIntegrationTest.class, SkipDirectRunnerTest.class}) +@TemplateIntegrationTest(SourceDbToSpanner.class) +@RunWith(JUnit4.class) +public class MySQLDDLIT extends SourceDbToSpannerITBase { + private static PipelineLauncher.LaunchInfo jobInfo; + + public static MySQLResourceManager mySQLResourceManager; + public static SpannerResourceManager spannerResourceManager; + + private static final String SESSION_FILE_RESOURCE = "DDLIT/company-session.json"; + private static final String MYSQL_DDL_RESOURCE = "DDLIT/company-mysql-schema.sql"; + private static final String SPANNER_DDL_RESOURCE = "DDLIT/company-spanner-schema.sql"; + + /** + * Setup resource managers and Launch dataflow job once during the execution of this test class. \ + */ + @Before + public void setUp() { + mySQLResourceManager = setUpMySQLResourceManager(); + spannerResourceManager = setUpSpannerResourceManager(); + } + + /** Cleanup dataflow job and all the resources and resource managers. */ + @After + public void cleanUp() { + ResourceManagerUtils.cleanResources(spannerResourceManager, mySQLResourceManager); + } + + @Test + public void ddlModificationTest() throws Exception { + loadSQLFileResource(mySQLResourceManager, MYSQL_DDL_RESOURCE); + createSpannerDDL(spannerResourceManager, SPANNER_DDL_RESOURCE); + jobInfo = + launchDataflowJob( + getClass().getSimpleName(), + SESSION_FILE_RESOURCE, + "mapper", + mySQLResourceManager, + spannerResourceManager, + null, + null); + PipelineOperator.Result result = pipelineOperator().waitUntilDone(createConfig(jobInfo)); + + List> companyMySQL = + mySQLResourceManager.runSQLQuery("SELECT company_id, company_name FROM company"); + ImmutableList companySpanner = + spannerResourceManager.readTableRecords("company", "company_id", "company_name"); + + SpannerAsserts.assertThatStructs(companySpanner) + .hasRecordsUnorderedCaseInsensitiveColumns(companyMySQL); + + List> employeeMySQL = + mySQLResourceManager.runSQLQuery( + "SELECT employee_id, company_id, employee_name, employee_address FROM employee"); + ImmutableList employeeSpanner = + spannerResourceManager.readTableRecords( + "employee", "employee_id", "company_id", "employee_name", "employee_address"); + + SpannerAsserts.assertThatStructs(employeeSpanner) + .hasRecordsUnorderedCaseInsensitiveColumns(employeeMySQL); + + ImmutableList employeeAttribute = + spannerResourceManager.readTableRecords( + "employee_attribute", "employee_id", "attribute_name", "value"); + + SpannerAsserts.assertThatStructs(employeeAttribute).hasRows(4); // Supports composite keys + + ImmutableList vendor = + spannerResourceManager.readTableRecords("vendor", "vendor_id", "full_name"); + + SpannerAsserts.assertThatStructs(vendor).hasRows(3); + } +} diff --git a/v2/sourcedb-to-spanner/src/test/resources/DDLIT/company-mysql-schema.sql b/v2/sourcedb-to-spanner/src/test/resources/DDLIT/company-mysql-schema.sql new file mode 100644 index 0000000000..9ac2fa4348 --- /dev/null +++ b/v2/sourcedb-to-spanner/src/test/resources/DDLIT/company-mysql-schema.sql @@ -0,0 +1,59 @@ +CREATE TABLE `company` ( + `company_id` int(11) PRIMARY KEY NOT NULL, + `company_name` varchar(100) DEFAULT NULL, + `created_on` date +); + +INSERT INTO `company` VALUES + (1,'gog','1998-09-04'), + (2,'app','1976-04-01'), + (3,'ama','1994-07-05'); + +CREATE TABLE `employee` ( + `employee_id` int(11) PRIMARY KEY NOT NULL, + `company_id` int(11) DEFAULT NULL, + `employee_name` varchar(100) DEFAULT NULL, + `employee_address` varchar(100) DEFAULT NULL, + `created_on` date +); + +INSERT INTO `employee` VALUES + (100,1,'emp1','add1','1996-01-01'), + (101,1,'emp2','add2','1999-01-01'), + (102,1,'emp3','add3','2012-01-01'), + (300,3,'emp300','add300','1996-01-01'); + +CREATE TABLE `employee_attribute` ( + `employee_id` int(11) NOT NULL, + `attribute_name` varchar(100) NOT NULL, + `value` varchar(100) DEFAULT NULL, + `updated_on` date, + PRIMARY KEY (`employee_id`,`attribute_name`) +); + +INSERT INTO `employee_attribute` VALUES + (100,'iq','150','2024-06-10'), + (101,'iq','120','2024-06-10'), + (102,'iq','20','2024-06-10'), + (300,'endurance','20','2024-06-10'); + +CREATE TABLE `vendor` ( + `vendor_id` INT AUTO_INCREMENT PRIMARY KEY, + `first_name` VARCHAR(255) NOT NULL, + `last_name` VARCHAR(255) NOT NULL, + `email` VARCHAR(255) UNIQUE NOT NULL, + `full_name` VARCHAR(512) GENERATED ALWAYS AS (CONCAT(first_name, ' ', last_name)), + INDEX full_name_idx (full_name) +); + +INSERT INTO vendor (vendor_id, first_name, last_name, email) VALUES + (1, 'David', 'Lee', 'david.lee@example.com'), + (2, 'Sarah', 'Jones', 'sarah.jones@example.com'), + (3, 'Michael', 'Brown', 'michael.brown@example.com'); + +CREATE TABLE `mysql_extra` ( + `test_id` int(11) PRIMARY KEY NOT NULL, + `test_name` varchar(100) DEFAULT NULL +); + +CREATE VIEW company_view AS SELECT company_id FROM company; \ No newline at end of file diff --git a/v2/sourcedb-to-spanner/src/test/resources/DDLIT/company-session.json b/v2/sourcedb-to-spanner/src/test/resources/DDLIT/company-session.json new file mode 100644 index 0000000000..2035c36279 --- /dev/null +++ b/v2/sourcedb-to-spanner/src/test/resources/DDLIT/company-session.json @@ -0,0 +1,968 @@ +{ + "SessionName": "NewSession", + "EditorName": "", + "DatabaseType": "mysql", + "DatabaseName": "company", + "Dialect": "google_standard_sql", + "Notes": null, + "Tags": null, + "SpSchema": { + "t1": { + "Name": "company", + "ColIds": [ + "c4", + "c5", + "c6" + ], + "ShardIdColumn": "", + "ColDefs": { + "c4": { + "Name": "company_id", + "T": { + "Name": "INT64", + "Len": 0, + "IsArray": false + }, + "NotNull": true, + "Comment": "From: company_id int(10)", + "Id": "c4", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c5": { + "Name": "company_name", + "T": { + "Name": "STRING", + "Len": 100, + "IsArray": false + }, + "NotNull": false, + "Comment": "From: company_name varchar(100)", + "Id": "c5", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c6": { + "Name": "created_on", + "T": { + "Name": "DATE", + "Len": 0, + "IsArray": false + }, + "NotNull": false, + "Comment": "From: created_on date", + "Id": "c6", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + } + }, + "PrimaryKeys": [ + { + "ColId": "c4", + "Desc": false, + "Order": 1 + } + ], + "ForeignKeys": null, + "Indexes": null, + "ParentId": "", + "Comment": "Spanner schema for source table company", + "Id": "t1" + }, + "t2": { + "Name": "employee", + "ColIds": [ + "c11", + "c12", + "c13", + "c14", + "c15" + ], + "ShardIdColumn": "", + "ColDefs": { + "c11": { + "Name": "employee_id", + "T": { + "Name": "INT64", + "Len": 0, + "IsArray": false + }, + "NotNull": true, + "Comment": "From: employee_id int(10)", + "Id": "c11", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c12": { + "Name": "company_id", + "T": { + "Name": "INT64", + "Len": 0, + "IsArray": false + }, + "NotNull": false, + "Comment": "From: company_id int(10)", + "Id": "c12", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c13": { + "Name": "employee_name", + "T": { + "Name": "STRING", + "Len": 100, + "IsArray": false + }, + "NotNull": false, + "Comment": "From: employee_name varchar(100)", + "Id": "c13", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c14": { + "Name": "employee_address", + "T": { + "Name": "STRING", + "Len": 100, + "IsArray": false + }, + "NotNull": false, + "Comment": "From: employee_address varchar(100)", + "Id": "c14", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c15": { + "Name": "created_on", + "T": { + "Name": "DATE", + "Len": 0, + "IsArray": false + }, + "NotNull": false, + "Comment": "From: created_on date", + "Id": "c15", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + } + }, + "PrimaryKeys": [ + { + "ColId": "c11", + "Desc": false, + "Order": 1 + } + ], + "ForeignKeys": null, + "Indexes": null, + "ParentId": "", + "Comment": "Spanner schema for source table employee", + "Id": "t2" + }, + "t3": { + "Name": "employee_attribute", + "ColIds": [ + "c7", + "c8", + "c9", + "c10" + ], + "ShardIdColumn": "", + "ColDefs": { + "c10": { + "Name": "updated_on", + "T": { + "Name": "DATE", + "Len": 0, + "IsArray": false + }, + "NotNull": false, + "Comment": "From: updated_on date", + "Id": "c10", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c7": { + "Name": "employee_id", + "T": { + "Name": "INT64", + "Len": 0, + "IsArray": false + }, + "NotNull": true, + "Comment": "From: employee_id int(10)", + "Id": "c7", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c8": { + "Name": "attribute_name", + "T": { + "Name": "STRING", + "Len": 100, + "IsArray": false + }, + "NotNull": true, + "Comment": "From: attribute_name varchar(100)", + "Id": "c8", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c9": { + "Name": "value", + "T": { + "Name": "STRING", + "Len": 100, + "IsArray": false + }, + "NotNull": false, + "Comment": "From: value varchar(100)", + "Id": "c9", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + } + }, + "PrimaryKeys": [ + { + "ColId": "c7", + "Desc": false, + "Order": 1 + }, + { + "ColId": "c8", + "Desc": false, + "Order": 2 + } + ], + "ForeignKeys": null, + "Indexes": null, + "ParentId": "", + "Comment": "Spanner schema for source table employee_attribute", + "Id": "t3" + }, + "t4": { + "Name": "vendor", + "ColIds": [ + "c11", + "c12", + "c13", + "c14", + "c15" + ], + "ShardIdColumn": "", + "ColDefs": { + "c11": { + "Name": "email", + "T": { + "Name": "STRING", + "Len": 255, + "IsArray": false + }, + "NotNull": true, + "Comment": "From: email varchar(255)", + "Id": "c11", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c12": { + "Name": "full_name", + "T": { + "Name": "STRING", + "Len": 512, + "IsArray": false + }, + "NotNull": false, + "Comment": "From: full_name varchar(512)", + "Id": "c12", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c13": { + "Name": "vendor_id", + "T": { + "Name": "INT64", + "Len": 0, + "IsArray": false + }, + "NotNull": true, + "Comment": "From: vendor_id int(10)", + "Id": "c13", + "AutoGen": { + "Name": "Sequence7", + "GenerationType": "Sequence" + } + }, + "c14": { + "Name": "first_name", + "T": { + "Name": "STRING", + "Len": 255, + "IsArray": false + }, + "NotNull": true, + "Comment": "From: first_name varchar(255)", + "Id": "c14", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c15": { + "Name": "last_name", + "T": { + "Name": "STRING", + "Len": 255, + "IsArray": false + }, + "NotNull": true, + "Comment": "From: last_name varchar(255)", + "Id": "c15", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + } + }, + "PrimaryKeys": [ + { + "ColId": "c12", + "Desc": false, + "Order": 1 + } + ], + "ForeignKeys": null, + "Indexes": [ + { + "Name": "full_name_idx", + "TableId": "t4", + "Unique": false, + "Keys": [ + { + "ColId": "c12", + "Desc": false, + "Order": 1 + } + ], + "Id": "i15", + "StoredColumnIds": null + }, + { + "Name": "email_idx", + "TableId": "t4", + "Unique": false, + "Keys": [ + { + "ColId": "c11", + "Desc": true, + "Order": 1 + } + ], + "Id": "i16", + "StoredColumnIds": null + } + ], + "ParentTable": { + "Id": "", + "OnDelete": "" + }, + "Comment": "Spanner schema for source table vendor", + "Id": "t4" + } + }, + "SyntheticPKeys": {}, + "SrcSchema": { + "t1": { + "Name": "company", + "Schema": "company", + "ColIds": [ + "c4", + "c5", + "c6" + ], + "ColDefs": { + "c4": { + "Name": "company_id", + "Type": { + "Name": "int", + "Mods": [ + 10 + ], + "ArrayBounds": null + }, + "NotNull": true, + "Ignored": { + "Check": false, + "Identity": false, + "Default": false, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c4" + }, + "c5": { + "Name": "company_name", + "Type": { + "Name": "varchar", + "Mods": [ + 100 + ], + "ArrayBounds": null + }, + "NotNull": false, + "Ignored": { + "Check": false, + "Identity": false, + "Default": true, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c5" + }, + "c6": { + "Name": "created_on", + "Type": { + "Name": "date", + "Mods": null, + "ArrayBounds": null + }, + "NotNull": false, + "Ignored": { + "Check": false, + "Identity": false, + "Default": true, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c6" + } + }, + "PrimaryKeys": [ + { + "ColId": "c4", + "Desc": false, + "Order": 1 + } + ], + "ForeignKeys": null, + "Indexes": null, + "Id": "t1" + }, + "t2": { + "Name": "employee", + "Schema": "company", + "ColIds": [ + "c11", + "c12", + "c13", + "c14", + "c15" + ], + "ColDefs": { + "c11": { + "Name": "employee_id", + "Type": { + "Name": "int", + "Mods": [ + 10 + ], + "ArrayBounds": null + }, + "NotNull": true, + "Ignored": { + "Check": false, + "Identity": false, + "Default": false, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c11" + }, + "c12": { + "Name": "company_id", + "Type": { + "Name": "int", + "Mods": [ + 10 + ], + "ArrayBounds": null + }, + "NotNull": false, + "Ignored": { + "Check": false, + "Identity": false, + "Default": true, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c12" + }, + "c13": { + "Name": "employee_name", + "Type": { + "Name": "varchar", + "Mods": [ + 100 + ], + "ArrayBounds": null + }, + "NotNull": false, + "Ignored": { + "Check": false, + "Identity": false, + "Default": true, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c13" + }, + "c14": { + "Name": "employee_address", + "Type": { + "Name": "varchar", + "Mods": [ + 100 + ], + "ArrayBounds": null + }, + "NotNull": false, + "Ignored": { + "Check": false, + "Identity": false, + "Default": true, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c14" + }, + "c15": { + "Name": "created_on", + "Type": { + "Name": "date", + "Mods": null, + "ArrayBounds": null + }, + "NotNull": false, + "Ignored": { + "Check": false, + "Identity": false, + "Default": true, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c15" + } + }, + "PrimaryKeys": [ + { + "ColId": "c11", + "Desc": false, + "Order": 1 + } + ], + "ForeignKeys": null, + "Indexes": null, + "Id": "t2" + }, + "t3": { + "Name": "employee_attribute", + "Schema": "company", + "ColIds": [ + "c7", + "c8", + "c9", + "c10" + ], + "ColDefs": { + "c10": { + "Name": "updated_on", + "Type": { + "Name": "date", + "Mods": null, + "ArrayBounds": null + }, + "NotNull": false, + "Ignored": { + "Check": false, + "Identity": false, + "Default": true, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c10" + }, + "c7": { + "Name": "employee_id", + "Type": { + "Name": "int", + "Mods": [ + 10 + ], + "ArrayBounds": null + }, + "NotNull": true, + "Ignored": { + "Check": false, + "Identity": false, + "Default": false, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c7" + }, + "c8": { + "Name": "attribute_name", + "Type": { + "Name": "varchar", + "Mods": [ + 100 + ], + "ArrayBounds": null + }, + "NotNull": true, + "Ignored": { + "Check": false, + "Identity": false, + "Default": false, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c8" + }, + "c9": { + "Name": "value", + "Type": { + "Name": "varchar", + "Mods": [ + 100 + ], + "ArrayBounds": null + }, + "NotNull": false, + "Ignored": { + "Check": false, + "Identity": false, + "Default": true, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c9" + } + }, + "PrimaryKeys": [ + { + "ColId": "c7", + "Desc": false, + "Order": 1 + }, + { + "ColId": "c8", + "Desc": false, + "Order": 2 + } + ], + "ForeignKeys": null, + "Indexes": null, + "Id": "t3" + }, + "t4": { + "Name": "vendor", + "Schema": "company", + "ColIds": [ + "c11", + "c12", + "c13", + "c14", + "c15" + ], + "ColDefs": { + "c11": { + "Name": "email", + "Type": { + "Name": "varchar", + "Mods": [ + 255 + ], + "ArrayBounds": null + }, + "NotNull": true, + "Ignored": { + "Check": false, + "Identity": false, + "Default": false, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c11", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c12": { + "Name": "full_name", + "Type": { + "Name": "varchar", + "Mods": [ + 512 + ], + "ArrayBounds": null + }, + "NotNull": false, + "Ignored": { + "Check": false, + "Identity": false, + "Default": false, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c12", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c13": { + "Name": "vendor_id", + "Type": { + "Name": "int", + "Mods": [ + 10 + ], + "ArrayBounds": null + }, + "NotNull": true, + "Ignored": { + "Check": false, + "Identity": false, + "Default": false, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c13", + "AutoGen": { + "Name": "Sequence7", + "GenerationType": "Auto Increment" + } + }, + "c14": { + "Name": "first_name", + "Type": { + "Name": "varchar", + "Mods": [ + 255 + ], + "ArrayBounds": null + }, + "NotNull": true, + "Ignored": { + "Check": false, + "Identity": false, + "Default": false, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c14", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + }, + "c15": { + "Name": "last_name", + "Type": { + "Name": "varchar", + "Mods": [ + 255 + ], + "ArrayBounds": null + }, + "NotNull": true, + "Ignored": { + "Check": false, + "Identity": false, + "Default": false, + "Exclusion": false, + "ForeignKey": false, + "AutoIncrement": false + }, + "Id": "c15", + "AutoGen": { + "Name": "", + "GenerationType": "" + } + } + }, + "PrimaryKeys": [ + { + "ColId": "c11", + "Desc": false, + "Order": 1 + } + ], + "ForeignKeys": null, + "Indexes": [ + { + "Name": "email", + "Unique": true, + "Keys": [ + { + "ColId": "c11", + "Desc": false, + "Order": 1 + } + ], + "Id": "i14", + "StoredColumnIds": null + }, + { + "Name": "full_name_idx", + "Unique": false, + "Keys": [ + { + "ColId": "c12", + "Desc": false, + "Order": 1 + } + ], + "Id": "i15", + "StoredColumnIds": null + } + ], + "Id": "t4" + } +}, + "SchemaIssues": { + "t1": { + "ColumnLevelIssues": { + "c4": [ + 14 + ], + "c5": [ + 0 + ], + "c6": [ + 0 + ] + }, + "TableLevelIssues": null + }, + "t2": { + "ColumnLevelIssues": { + "c11": [ + 14 + ], + "c12": [ + 14, + 0 + ], + "c13": [ + 0 + ], + "c14": [ + 0 + ], + "c15": [ + 0 + ] + }, + "TableLevelIssues": null + }, + "t3": { + "ColumnLevelIssues": { + "c10": [ + 0 + ], + "c7": [ + 14 + ], + "c9": [ + 0 + ] + }, + "TableLevelIssues": null + } + }, + "Location": {}, + "TimezoneOffset": "+00:00", + "SpDialect": "google_standard_sql", + "UniquePKey": {}, + "Rules": [], + "IsSharded": false, + "SpRegion": "", + "ResourceValidation": false, + "UI": false, + "SpSequences": { + "s7": { + "Id": "s7", + "Name": "Sequence7", + "SequenceKind": "BIT REVERSED POSITIVE", + "SkipRangeMin": "", + "SkipRangeMax": "", + "StartWithCounter": "", + "ColumnsUsingSeq": { + "t2": [ + "c6" + ] + } + } + }, + "SrcSequences": { + "s7": { + "Id": "s7", + "Name": "Sequence7", + "SequenceKind": "BIT REVERSED SEQUENCE", + "SkipRangeMin": "", + "SkipRangeMax": "", + "StartWithCounter": "", + "ColumnsUsingSeq": { + "t2": [ + "c6" + ] + } + } + } +} \ No newline at end of file diff --git a/v2/sourcedb-to-spanner/src/test/resources/DDLIT/company-spanner-schema.sql b/v2/sourcedb-to-spanner/src/test/resources/DDLIT/company-spanner-schema.sql new file mode 100644 index 0000000000..4855166cfe --- /dev/null +++ b/v2/sourcedb-to-spanner/src/test/resources/DDLIT/company-spanner-schema.sql @@ -0,0 +1,43 @@ +CREATE TABLE + company +( + company_id INT64 NOT NULL, + company_name STRING(100), + created_on DATE, +) PRIMARY KEY + (company_id); +CREATE TABLE + employee +( + employee_id INT64 NOT NULL, + company_id INT64, + employee_name STRING(100), + employee_address STRING(100), + created_on DATE, +) PRIMARY KEY + (employee_id); + +CREATE TABLE + employee_attribute +( + employee_id INT64 NOT NULL, + attribute_name STRING(100) NOT NULL, + value STRING(100), + updated_on DATE, +) PRIMARY KEY + (employee_id, attribute_name); + +CREATE SEQUENCE Sequence7 OPTIONS (sequence_kind = 'bit_reversed_positive'); + +CREATE TABLE vendor ( + vendor_id INT64 NOT NULL DEFAULT (GET_NEXT_SEQUENCE_VALUE(SEQUENCE Sequence7)), + first_name STRING(255) NOT NULL, + last_name STRING(255) NOT NULL, + email STRING(255) NOT NULL, + full_name STRING(512), +) PRIMARY KEY (full_name); + +CREATE INDEX full_name_idx ON vendor (full_name); +CREATE INDEX email_idx ON vendor (email DESC); + +CREATE VIEW company_view SQL SECURITY DEFINER AS SELECT company.company_id FROM company; \ No newline at end of file diff --git a/v2/spanner-to-sourcedb/src/main/java/com/google/cloud/teleport/v2/templates/dbutils/dml/CassandraTypeHandler.java b/v2/spanner-to-sourcedb/src/main/java/com/google/cloud/teleport/v2/templates/dbutils/dml/CassandraTypeHandler.java new file mode 100644 index 0000000000..54edd291dd --- /dev/null +++ b/v2/spanner-to-sourcedb/src/main/java/com/google/cloud/teleport/v2/templates/dbutils/dml/CassandraTypeHandler.java @@ -0,0 +1,974 @@ +/* + * Copyright (C) 2024 Google LLC + * + * Licensed 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 com.google.cloud.teleport.v2.templates.dbutils.dml; + +import com.google.common.net.InetAddresses; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +class CassandraTypeHandler { + + @FunctionalInterface + public interface TypeParser { + T parse(Object value); + } + + /** + * Converts a {@link String} to an ASCII representation for Cassandra's {@link String} or other + * ASCII-based types. + * + *

This method ensures that the string contains only valid ASCII characters (0-127). If any + * non-ASCII characters are found, an exception is thrown. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing all the key-value pairs for the current + * incoming stream. + * @return A {@link String} representing the ASCII value for the column in Cassandra. + * @throws IllegalArgumentException If the string contains non-ASCII characters. + */ + public static String handleCassandraAsciiType(String colName, JSONObject valuesJson) { + Object value = valuesJson.get(colName); + if (value instanceof String) { + String stringValue = (String) value; + if (isAscii(stringValue)) { + return stringValue; + } else { + throw new IllegalArgumentException( + "Invalid ASCII format for column: " + + colName + + ". String contains non-ASCII characters."); + } + } + return null; + } + + /** + * Generates a {@link BigInteger} based on the provided {@link CassandraTypeHandler}. + * + *

This method fetches the value associated with the given column name ({@code colName}) from + * the {@code valuesJson} object, and converts it to a {@link BigInteger}. The value can either be + * a string representing a number or a binary representation of a large integer (varint). + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing all the key-value pairs for the current + * incoming stream. + * @return A {@link BigInteger} object representing the varint value from the Cassandra data. + * @throws IllegalArgumentException If the value is not a valid format for varint (neither a valid + * number string nor a byte array). + */ + public static BigInteger handleCassandraVarintType(String colName, JSONObject valuesJson) { + Object value = valuesJson.get(colName); + + if (value instanceof String) { + try { + return new BigInteger((String) value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Invalid varint format (string) for column: " + colName, e); + } + } else if (value instanceof byte[]) { + try { + return new BigInteger((byte[]) value); + } catch (Exception e) { + throw new IllegalArgumentException( + "Invalid varint format (byte array) for column: " + colName, e); + } + } else { + return null; + } + } + + /** + * Generates a {@link Duration} based on the provided {@link CassandraTypeHandler}. + * + *

This method fetches a string value from the provided {@code valuesJson} object using the + * column name {@code colName}, and converts it into a {@link Duration} object. The string value + * should be in the ISO-8601 duration format (e.g., "PT20.345S"). + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing all the key-value pairs for the current + * incoming stream. + * @return A {@link Duration} object representing the duration value from the Cassandra data. + * @throws IllegalArgumentException if the value is not a valid duration string. + */ + public static Duration handleCassandraDurationType(String colName, JSONObject valuesJson) { + String durationString = valuesJson.optString(colName, null); + if (durationString == null) { + return null; + } + try { + return Duration.parse(durationString); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid duration format for column: " + colName, e); + } + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link InetAddress} object containing InetAddress as value represented in cassandra + * type. + */ + public static InetAddress handleCassandraInetAddressType(String colName, JSONObject valuesJson) { + String inetString = valuesJson.optString(colName, null); + if (inetString == null) { + return null; + } + try { + return InetAddresses.forString(inetString); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid IP address format for column: " + colName, e); + } + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link Boolean} object containing the value represented in cassandra type. + */ + public static Boolean handleCassandraBoolType(String colName, JSONObject valuesJson) { + return valuesJson.optBoolean(colName, false); + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link Float} object containing the value represented in cassandra type. + */ + public static Float handleCassandraFloatType(String colName, JSONObject valuesJson) { + try { + return valuesJson.getBigDecimal(colName).floatValue(); + } catch (JSONException e) { + return null; + } + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link Double} object containing the value represented in cassandra type. + */ + public static Double handleCassandraDoubleType(String colName, JSONObject valuesJson) { + try { + return valuesJson.getBigDecimal(colName).doubleValue(); + } catch (JSONException e) { + return null; + } + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link ByteBuffer} object containing the value represented in cassandra type. + */ + public static ByteBuffer handleCassandraBlobType(String colName, JSONObject valuesJson) { + Object colValue = valuesJson.opt(colName); + if (colValue == null) { + return null; + } + return parseBlobType(colValue); + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colValue - contains all the key value for current incoming stream. + * @return a {@link ByteBuffer} object containing the value represented in cassandra type. + */ + public static ByteBuffer parseBlobType(Object colValue) { + byte[] byteArray; + if (colValue instanceof byte[]) { + byteArray = (byte[]) colValue; + } else if (colValue instanceof String) { + byteArray = java.util.Base64.getDecoder().decode((String) colValue); + } else { + throw new IllegalArgumentException("Unsupported type for column"); + } + return ByteBuffer.wrap(byteArray); + } + + /** + * Generates a {@link LocalDate} based on the provided {@link CassandraTypeHandler}. + * + *

This method processes the given JSON object to extract a date value using the specified + * column name and formatter. It specifically handles the "Cassandra Date" format (yyyy-MM-dd). + * The resulting {@link LocalDate} represents the date value associated with the column. + * + * @param colName - the key used to fetch the value from the provided {@link JSONObject}. + * @param valuesJson - the JSON object containing all key-value pairs for the current incoming + * data stream. + * @return a {@link LocalDate} object containing the date value represented in Cassandra type + * format. If the column is missing or contains an invalid value, this will return {@code + * null}. + */ + public static LocalDate handleCassandraDateType(String colName, JSONObject valuesJson) { + return handleCassandraGenericDateType(colName, valuesJson, "yyyy-MM-dd"); + } + + /** + * Parses a timestamp value from a JSON object and returns it as an {@link Instant} in UTC. + * + *

This method extracts a timestamp value associated with the given column name from the + * provided {@link JSONObject}. The timestamp is expected to be in an ISO-8601 compatible format + * (e.g., "yyyy-MM-dd'T'HH:mm:ss.SSSZ"). The method ensures that the returned {@link Instant} is + * always in UTC, regardless of the time zone present in the input. + * + *

If the input timestamp cannot be parsed directly as an {@link Instant}, the method attempts + * to parse it as a {@link ZonedDateTime} and normalizes it to UTC before converting it to an + * {@link Instant}. If parsing fails, an {@link IllegalArgumentException} is thrown. + * + *

This method is particularly useful for processing timestamp data stored in Cassandra, where + * timestamps are often stored as ISO-8601 strings. + * + * @param colName the key used to fetch the value from the provided {@link JSONObject}. + * @param valuesJson the JSON object containing key-value pairs, including the timestamp value. + * @return an {@link Instant} representing the parsed timestamp value in UTC. + * @throws IllegalArgumentException if the column value is missing, empty, or cannot be parsed as + * a valid timestamp. + */ + public static Instant handleCassandraTimestampType(String colName, JSONObject valuesJson) { + String timestampValue = valuesJson.optString(colName, null); + if (timestampValue == null || timestampValue.isEmpty()) { + throw new IllegalArgumentException( + "Timestamp value for column " + colName + " is null or empty."); + } + return convertToCassandraTimestamp(timestampValue); + } + + /** + * A helper method that handles the conversion of a given column value to a {@link LocalDate} + * based on the specified date format (formatter). + * + *

This method extracts the value for the given column name from the provided JSON object and + * parses it into a {@link LocalDate} based on the provided date format. If the value is in an + * unsupported type or format, an exception is thrown. + * + * @param colName - the key used to fetch the value from the provided {@link JSONObject}. + * @param valuesJson - the JSON object containing all key-value pairs for the current incoming + * data stream. + * @param formatter - the date format pattern used to parse the value (e.g., "yyyy-MM-dd"). + * @return a {@link LocalDate} object containing the parsed date value. If the column is missing + * or invalid, this method returns {@code null}. + */ + public static LocalDate handleCassandraGenericDateType( + String colName, JSONObject valuesJson, String formatter) { + Object colValue = valuesJson.opt(colName); + if (colValue == null) { + return null; + } + + if (formatter == null) { + formatter = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + } + + return parseDate(colName, colValue, formatter); + } + + /** + * Parses a column value (String, {@link java.util.Date}, or {@code Long}) into a {@link + * LocalDate} using the specified date format. + * + *

This method handles different data types (String, Date, Long) by converting them into a + * {@link LocalDate}. The provided formatter is used to parse date strings, while other types are + * converted based on their corresponding representations. + * + * @param colName - the key used to fetch the value from the provided {@link JSONObject}. + * @param colValue - the value to be parsed into a {@link LocalDate}. + * @param formatter - the date format pattern used to parse date strings. + * @return a {@link LocalDate} object parsed from the given value. + * @throws IllegalArgumentException if the value cannot be parsed or is of an unsupported type. + */ + public static LocalDate parseDate(String colName, Object colValue, String formatter) { + LocalDate localDate; + if (colValue instanceof String) { + try { + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(formatter); + localDate = LocalDate.parse((String) colValue, dateFormatter); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Invalid date format for column " + colName, e); + } + } else if (colValue instanceof java.util.Date) { + localDate = + ((java.util.Date) colValue) + .toInstant() + .atZone(java.time.ZoneId.systemDefault()) + .toLocalDate(); + } else if (colValue instanceof Long) { + localDate = + java.time.Instant.ofEpochMilli((Long) colValue) + .atZone(java.time.ZoneId.systemDefault()) + .toLocalDate(); + } else { + throw new IllegalArgumentException("Unsupported type for column " + colName); + } + return localDate; + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link String} object containing String as value represented in cassandra type. + */ + public static String handleCassandraTextType(String colName, JSONObject valuesJson) { + return valuesJson.optString( + colName, null); // Get the value or null if the key is not found or the value is null + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link UUID} object containing UUID as value represented in cassandra type. + */ + public static UUID handleCassandraUuidType(String colName, JSONObject valuesJson) { + String uuidString = + valuesJson.optString( + colName, null); // Get the value or null if the key is not found or the value is null + + if (uuidString == null) { + return null; + } + + return UUID.fromString(uuidString); + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link Long} object containing Long as value represented in cassandra type. + */ + public static Long handleCassandraBigintType(String colName, JSONObject valuesJson) { + try { + return valuesJson.getBigInteger(colName).longValue(); + } catch (JSONException e) { + return null; + } + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link Integer} object containing Integer as value represented in cassandra type. + */ + public static Integer handleCassandraIntType(String colName, JSONObject valuesJson) { + try { + return valuesJson.getBigInteger(colName).intValue(); + } catch (JSONException e) { + return null; + } + } + + /** + * Generates a {@link List} object containing a list of long values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link List} object containing a list of long values represented in Cassandra. + */ + public static List handleInt64ArrayType(String colName, JSONObject valuesJson) { + return handleArrayType( + colName, + valuesJson, + obj -> { + if (obj instanceof Long) { + return (Long) obj; + } else if (obj instanceof Number) { + return ((Number) obj).longValue(); + } else if (obj instanceof String) { + try { + return Long.getLong((String) obj); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid number format for column " + colName, e); + } + } else { + throw new IllegalArgumentException("Unsupported type for column " + colName); + } + }); + } + + /** + * Generates a {@link Set} object containing a set of long values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link Set} object containing a set of long values represented in Cassandra. + */ + public static Set handleInt64SetType(String colName, JSONObject valuesJson) { + return new HashSet<>(handleInt64ArrayType(colName, valuesJson)); + } + + /** + * Generates a {@link List} object containing a list of integer values from Cassandra by + * converting long values to int. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link List} object containing a list of integer values represented in Cassandra. + */ + public static List handleInt64ArrayAsInt32Array(String colName, JSONObject valuesJson) { + return handleInt64ArrayType(colName, valuesJson).stream() + .map(Long::intValue) + .collect(Collectors.toList()); + } + + /** + * Generates a {@link Set} object containing a set of integer values from Cassandra by converting + * long values to int. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link Set} object containing a set of integer values represented in Cassandra. + */ + public static Set handleInt64ArrayAsInt32Set(String colName, JSONObject valuesJson) { + return handleInt64ArrayType(colName, valuesJson).stream() + .map(Long::intValue) + .collect(Collectors.toSet()); + } + + /** + * Generates a {@link Set} object containing a set of string values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link Set} object containing a set of string values represented in Cassandra. + */ + public static Set handleStringSetType(String colName, JSONObject valuesJson) { + return new HashSet<>(handleStringArrayType(colName, valuesJson)); + } + + /** + * Generates a {@link List} object containing a list of string values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link List} object containing a list of string values represented in Cassandra. + */ + public static List handleStringArrayType(String colName, JSONObject valuesJson) { + return handleArrayType(colName, valuesJson, String::valueOf); + } + + /** + * Generates a {@link List} object containing a list of boolean values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link List} object containing a list of boolean values represented in Cassandra. + */ + public static List handleBoolArrayType(String colName, JSONObject valuesJson) { + return handleArrayType( + colName, valuesJson, obj -> obj instanceof String && Boolean.parseBoolean((String) obj)); + } + + /** + * Generates a {@link Set} object containing a set of boolean values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link Set} object containing a set of boolean values represented in Cassandra. + */ + public static Set handleBoolSetTypeString(String colName, JSONObject valuesJson) { + return new HashSet<>(handleBoolArrayType(colName, valuesJson)); + } + + /** + * Generates a {@link List} object containing a list of double values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link List} object containing a list of double values represented in Cassandra. + */ + public static List handleFloat64ArrayType(String colName, JSONObject valuesJson) { + return handleArrayType( + colName, + valuesJson, + obj -> { + if (obj instanceof Number) { + return ((Number) obj).doubleValue(); + } else if (obj instanceof String) { + try { + return Double.valueOf((String) obj); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid number format for column " + colName, e); + } + } else { + throw new IllegalArgumentException("Unsupported type for column " + colName); + } + }); + } + + /** + * Generates a {@link Set} object containing a set of double values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link Set} object containing a set of double values represented in Cassandra. + */ + public static Set handleFloat64SetType(String colName, JSONObject valuesJson) { + return new HashSet<>(handleFloat64ArrayType(colName, valuesJson)); + } + + /** + * Generates a {@link List} object containing a list of float values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link List} object containing a list of float values represented in Cassandra. + */ + public static List handleFloatArrayType(String colName, JSONObject valuesJson) { + return handleFloat64ArrayType(colName, valuesJson).stream() + .map(Double::floatValue) + .collect(Collectors.toList()); + } + + /** + * Generates a {@link Set} object containing a set of float values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link Set} object containing a set of float values represented in Cassandra. + */ + public static Set handleFloatSetType(String colName, JSONObject valuesJson) { + return handleFloat64SetType(colName, valuesJson).stream() + .map(Double::floatValue) + .collect(Collectors.toSet()); + } + + /** + * Generates a {@link List} object containing a list of LocalDate values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link List} object containing a list of LocalDate values represented in Cassandra. + */ + public static List handleDateArrayType(String colName, JSONObject valuesJson) { + return handleArrayType( + colName, valuesJson, obj -> LocalDate.parse(obj.toString(), DateTimeFormatter.ISO_DATE)); + } + + /** + * Generates a {@link Set} object containing a set of LocalDate values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link Set} object containing a set of LocalDate values represented in Cassandra. + */ + public static Set handleDateSetType(String colName, JSONObject valuesJson) { + return new HashSet<>(handleDateArrayType(colName, valuesJson)); + } + + /** + * Generates a {@link List} object containing a list of Timestamp values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link List} object containing a list of Timestamp values represented in Cassandra. + */ + public static List handleTimestampArrayType(String colName, JSONObject valuesJson) { + return handleArrayType( + colName, + valuesJson, + value -> + Timestamp.valueOf( + parseDate(colName, value, "yyyy-MM-dd'T'HH:mm:ss.SSSX").atStartOfDay())); + } + + /** + * Generates a {@link Set} object containing a set of Timestamp values from Cassandra. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing key-value pairs for the current incoming + * stream. + * @return a {@link Set} object containing a set of Timestamp values represented in Cassandra. + */ + public static Set handleTimestampSetType(String colName, JSONObject valuesJson) { + return new HashSet<>(handleTimestampArrayType(colName, valuesJson)); + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link List} object containing List of ByteBuffer as value represented in cassandra + * type. + */ + public static List handleByteArrayType(String colName, JSONObject valuesJson) { + return handleArrayType(colName, valuesJson, CassandraTypeHandler::parseBlobType); + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link List} object containing List of Type T as value represented in cassandra type + * which will be assigned runtime. + */ + public static List handleArrayType( + String colName, JSONObject valuesJson, TypeParser parser) { + return valuesJson.getJSONArray(colName).toList().stream() + .map(parser::parse) + .collect(Collectors.toList()); + } + + /** + * Generates a Type based on the provided {@link CassandraTypeHandler}. + * + * @param colName - which is used to fetch Key from valueJSON. + * @param valuesJson - contains all the key value for current incoming stream. + * @return a {@link Set} object containing Set of ByteBuffer as value represented in cassandra + * type. + */ + public static Set handleByteSetType(String colName, JSONObject valuesJson) { + return new HashSet<>(handleByteArrayType(colName, valuesJson)); + } + + /** + * Converts a stringified JSON object to a {@link Map} representation for Cassandra. + * + *

This method fetches the value associated with the given column name ({@code colName}) from + * the {@code valuesJson} object, parses the stringified JSON, and returns it as a {@link Map}. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing all the key-value pairs for the current + * incoming stream. + * @return A {@link Map} representing the parsed JSON from the stringified JSON. + * @throws IllegalArgumentException If the value is not a valid stringified JSON or cannot be + * parsed. + */ + public static Map handleStringifiedJsonToMap( + String colName, JSONObject valuesJson) { + Object value = valuesJson.get(colName); + if (value instanceof String) { + String jsonString = (String) value; + try { + JSONObject jsonObject = new JSONObject(jsonString); + Map map = new HashMap<>(); + for (String key : jsonObject.keySet()) { + Object jsonValue = jsonObject.get(key); + if (jsonValue instanceof JSONArray) { + map.put(key, jsonObject.getJSONArray(key)); + } else if (jsonValue instanceof JSONObject) { + map.put(key, jsonObject.getJSONObject(key)); + } else { + map.put(key, jsonValue); + } + } + return map; + } catch (Exception e) { + throw new IllegalArgumentException( + "Invalid stringified JSON format for column: " + colName, e); + } + } else { + throw new IllegalArgumentException( + "Invalid format for column: " + colName + ". Expected a stringified JSON."); + } + } + + /** + * Converts a stringified JSON array to a {@link List} representation for Cassandra. + * + *

This method fetches the value associated with the given column name ({@code colName}) from + * the {@code valuesJson} object, parses the stringified JSON array, and returns it as a {@link + * List}. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing all the key-value pairs for the current + * incoming stream. + * @return A {@link List} representing the parsed JSON array from the stringified JSON. + * @throws IllegalArgumentException If the value is not a valid stringified JSON array or cannot + * be parsed. + */ + public static List handleStringifiedJsonToList(String colName, JSONObject valuesJson) { + Object value = valuesJson.get(colName); + if (value instanceof String) { + String jsonString = (String) value; + try { + JSONArray jsonArray = new JSONArray(jsonString); + List list = new ArrayList<>(); + for (int i = 0; i < jsonArray.length(); i++) { + list.add(jsonArray.get(i)); + } + return list; + } catch (Exception e) { + throw new IllegalArgumentException( + "Invalid stringified JSON array format for column: " + colName, e); + } + } else { + throw new IllegalArgumentException( + "Invalid format for column: " + colName + ". Expected a stringified JSON array."); + } + } + + /** + * Converts a stringified JSON array to a {@link Set} representation for Cassandra. + * + *

This method fetches the value associated with the given column name ({@code colName}) from + * the {@code valuesJson} object, parses the stringified JSON array, and returns it as a {@link + * Set}. + * + * @param colName - The column name used to fetch the key from {@code valuesJson}. + * @param valuesJson - The {@link JSONObject} containing all the key-value pairs for the current + * incoming stream. + * @return A {@link Set} representing the parsed JSON array from the stringified JSON. + * @throws IllegalArgumentException If the value is not a valid stringified JSON array or cannot + * be parsed. + */ + public static Set handleStringifiedJsonToSet(String colName, JSONObject valuesJson) { + return new HashSet<>(handleStringifiedJsonToList(colName, valuesJson)); + } + + /** + * Converts an {@link Integer} to a {@code short} (SmallInt). + * + *

This method checks if the {@code integerValue} is within the valid range for a {@code + * smallint} (i.e., between {@link Short#MIN_VALUE} and {@link Short#MAX_VALUE}). If the value is + * out of range, it throws an {@link IllegalArgumentException}. + * + * @param integerValue The integer value to be converted. + * @return The converted {@code short} value. + * @throws IllegalArgumentException If the {@code integerValue} is out of range for a {@code + * smallint}. + */ + public static short convertToSmallInt(Integer integerValue) { + if (integerValue < Short.MIN_VALUE || integerValue > Short.MAX_VALUE) { + throw new IllegalArgumentException("Value is out of range for smallint."); + } + return integerValue.shortValue(); + } + + /** + * Converts an {@link Integer} to a {@code byte} (TinyInt). + * + *

This method checks if the {@code integerValue} is within the valid range for a {@code + * tinyint} (i.e., between {@link Byte#MIN_VALUE} and {@link Byte#MAX_VALUE}). If the value is out + * of range, it throws an {@link IllegalArgumentException}. + * + * @param integerValue The integer value to be converted. + * @return The converted {@code byte} value. + * @throws IllegalArgumentException If the {@code integerValue} is out of range for a {@code + * tinyint}. + */ + public static byte convertToTinyInt(Integer integerValue) { + if (integerValue < Byte.MIN_VALUE || integerValue > Byte.MAX_VALUE) { + throw new IllegalArgumentException("Value is out of range for tinyint."); + } + return integerValue.byteValue(); + } + + /** + * Escapes single quotes in a Cassandra string by replacing them with double single quotes. + * + *

This method is commonly used to sanitize strings before inserting them into Cassandra + * queries, where single quotes need to be escaped by doubling them (i.e., `'` becomes `''`). + * + * @param value The string to be escaped. + * @return The escaped string where single quotes are replaced with double single quotes. + */ + public static String escapeCassandraString(String value) { + return value.replace("'", "''"); + } + + /** + * Converts a string representation of a timestamp to a Cassandra-compatible timestamp. + * + *

The method parses the {@code value} as a {@link ZonedDateTime}, applies the given timezone + * offset to adjust the time, and converts the result into a UTC timestamp string that is + * compatible with Cassandra. + * + * @param value The timestamp string in ISO-8601 format (e.g., "2024-12-05T10:15:30+01:00"). + * @param timezoneOffset The timezone offset (e.g., "+02:00") to apply to the timestamp. + * @return A string representation of the timestamp in UTC that is compatible with Cassandra. + * @throws RuntimeException If the timestamp string is invalid or the conversion fails. + */ + public static String convertToCassandraTimestamp(String value, String timezoneOffset) { + try { + ZonedDateTime dateTime = ZonedDateTime.parse(value); + ZoneOffset offset = ZoneOffset.of(timezoneOffset); + dateTime = dateTime.withZoneSameInstant(offset); + return "'" + dateTime.withZoneSameInstant(ZoneOffset.UTC).toString() + "'"; + } catch (DateTimeParseException e) { + throw new RuntimeException(e); + } + } + + /** + * Converts a string representation of a date to a {@link LocalDate} compatible with Cassandra. + * + *

The method parses the {@code dateString} into an {@link Instant}, converts it to a {@link + * Date}, and then retrieves the corresponding {@link LocalDate} from the system's default time + * zone. + * + * @param dateString The date string in ISO-8601 format (e.g., "2024-12-05T00:00:00Z"). + * @return The {@link LocalDate} representation of the date. + */ + public static LocalDate convertToCassandraDate(String dateString) { + Instant instant = convertToCassandraTimestamp(dateString); + ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault()); + return zonedDateTime.toLocalDate(); + } + + /** + * Converts a string representation of a timestamp to an {@link Instant} compatible with + * Cassandra. + * + *

The method parses the {@code dateString} into an {@link Instant}, which represents an + * instantaneous point in time and is compatible with Cassandra timestamp types. + * + * @param timestampValue The timestamp string in ISO-8601 format (e.g., "2024-12-05T10:15:30Z"). + * @return The {@link Instant} representation of the timestamp. + */ + public static Instant convertToCassandraTimestamp(String timestampValue) { + try { + return Instant.parse(timestampValue); + } catch (DateTimeParseException e) { + try { + return ZonedDateTime.parse(timestampValue) + .withZoneSameInstant(java.time.ZoneOffset.UTC) + .toInstant(); + } catch (DateTimeParseException nestedException) { + throw new IllegalArgumentException( + "Failed to parse timestamp value" + timestampValue, nestedException); + } + } + } + + /** + * Validates if the given string represents a valid UUID. + * + *

This method attempts to parse the provided string as a UUID using {@link + * UUID#fromString(String)}. If parsing is successful, it returns {@code true}, indicating that + * the string is a valid UUID. Otherwise, it returns {@code false}. + * + * @param value The string to check if it represents a valid UUID. + * @return {@code true} if the string is a valid UUID, {@code false} otherwise. + */ + public static boolean isValidUUID(String value) { + try { + UUID.fromString(value); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + /** + * Validates if the given string represents a valid IP address. + * + *

This method attempts to resolve the provided string as an {@link InetAddresses} using {@link + * InetAddresses#forString(String)}. If successful, it returns {@code true}, indicating that the + * string is a valid IP address. Otherwise, it returns {@code false}. + * + * @param value The string to check if it represents a valid IP address. + * @return {@code true} if the string is a valid IP address, {@code false} otherwise. + */ + public static boolean isValidIPAddress(String value) { + try { + InetAddresses.forString(value); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Validates if the given string is a valid JSON object. + * + *

This method attempts to parse the string using {@link JSONObject} to check if the value + * represents a valid JSON object. If the string is valid JSON, it returns {@code true}, otherwise + * {@code false}. + * + * @param value The string to check if it represents a valid JSON object. + * @return {@code true} if the string is a valid JSON object, {@code false} otherwise. + */ + public static boolean isValidJSON(String value) { + try { + new JSONObject(value); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Helper method to check if a string contains only ASCII characters (0-127). + * + * @param value - The string to check. + * @return true if the string contains only ASCII characters, false otherwise. + */ + public static boolean isAscii(String value) { + for (int i = 0; i < value.length(); i++) { + if (value.charAt(i) > 127) { + return false; + } + } + return true; + } +} diff --git a/v2/spanner-to-sourcedb/src/test/java/com/google/cloud/teleport/v2/templates/dbutils/dml/CassandraTypeHandlerTest.java b/v2/spanner-to-sourcedb/src/test/java/com/google/cloud/teleport/v2/templates/dbutils/dml/CassandraTypeHandlerTest.java new file mode 100644 index 0000000000..33bd3cfece --- /dev/null +++ b/v2/spanner-to-sourcedb/src/test/java/com/google/cloud/teleport/v2/templates/dbutils/dml/CassandraTypeHandlerTest.java @@ -0,0 +1,1317 @@ +/* + * Copyright (C) 2024 Google LLC + * + * Licensed 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 com.google.cloud.teleport.v2.templates.dbutils.dml; + +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.convertToCassandraDate; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.convertToCassandraTimestamp; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.convertToSmallInt; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.convertToTinyInt; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.escapeCassandraString; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleBoolSetTypeString; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleByteArrayType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleByteSetType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraAsciiType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraBigintType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraBlobType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraBoolType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraDateType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraDoubleType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraDurationType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraFloatType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraInetAddressType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraIntType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraTextType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraTimestampType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraUuidType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleCassandraVarintType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleDateSetType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleFloat64ArrayType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleFloatArrayType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleFloatSetType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleInt64ArrayAsInt32Array; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleInt64ArrayAsInt32Set; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleInt64SetType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleStringArrayType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleStringSetType; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleStringifiedJsonToMap; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.handleStringifiedJsonToSet; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.isValidIPAddress; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.isValidJSON; +import static com.google.cloud.teleport.v2.templates.dbutils.dml.CassandraTypeHandler.isValidUUID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Base64; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.json.JSONObject; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CassandraTypeHandlerTest { + + @Test + public void convertSpannerValueJsonToBooleanType() { + String newValuesString = "{\"FirstName\":\"kk\",\"LastName\":\"ll\", \"isAdmin\":\"true\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "isAdmin"; + Boolean convertedValue = handleCassandraBoolType(colKey, newValuesJson); + assertTrue(convertedValue); + } + + @Test + public void convertSpannerValueJsonToBooleanType_False() { + String newValuesString = "{\"FirstName\":\"kk\",\"LastName\":\"ll\", \"isAdmin\":\"false\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "isAdmin"; + Boolean convertedValue = handleCassandraBoolType(colKey, newValuesJson); + Assert.assertFalse(convertedValue); + } + + @Test + public void convertSpannerValueJsonToFloatType() { + String newValuesString = "{\"FirstName\":\"kk\",\"LastName\":\"ll\", \"age\":23.5}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "age"; + Float convertedValue = handleCassandraFloatType(colKey, newValuesJson); + assertEquals(23.5f, convertedValue, 0.01f); + } + + @Test + public void convertSpannerValueJsonToDoubleType() { + String newValuesString = "{\"FirstName\":\"kk\",\"LastName\":\"ll\", \"salary\":100000.75}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "salary"; + Double convertedValue = handleCassandraDoubleType(colKey, newValuesJson); + assertEquals(100000.75, convertedValue, 0.01); + } + + @Test + public void convertSpannerValueJsonToBlobType_FromByteArray() { + String newValuesString = + "{\"FirstName\":\"kk\",\"LastName\":\"ll\", \"data\":\"QUJDQDEyMzQ=\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "data"; + ByteBuffer convertedValue = handleCassandraBlobType(colKey, newValuesJson); + byte[] expectedBytes = java.util.Base64.getDecoder().decode("QUJDQDEyMzQ="); + byte[] actualBytes = new byte[convertedValue.remaining()]; + convertedValue.get(actualBytes); + Assert.assertArrayEquals(expectedBytes, actualBytes); + } + + @Rule public ExpectedException expectedEx = ExpectedException.none(); + + @Test + public void testHandleNullBooleanType() { + String newValuesString = "{\"isAdmin\":null}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "isAdmin"; + assertEquals(false, handleCassandraBoolType(colKey, newValuesJson)); + } + + @Test + public void testHandleNullFloatType() { + String newValuesString = "{\"age\":null}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "age"; + assertNull(handleCassandraFloatType(colKey, newValuesJson)); + } + + @Test + public void testHandleNullDoubleType() { + String newValuesString = "{\"salary\":null}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "salary"; + Double value = handleCassandraDoubleType(colKey, newValuesJson); + assertNull(value); + } + + @Test + public void testHandleMaxInteger() { + String newValuesString = "{\"age\":2147483647}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "age"; + Integer value = handleCassandraIntType(colKey, newValuesJson); + assertEquals(Integer.MAX_VALUE, value.longValue()); + } + + @Test + public void testHandleMinInteger() { + String newValuesString = "{\"age\":-2147483648}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "age"; + Integer value = handleCassandraIntType(colKey, newValuesJson); + assertEquals(Integer.MIN_VALUE, value.longValue()); + } + + @Test + public void testHandleMaxLong() { + String newValuesString = "{\"age\":9223372036854775807}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "age"; + Long value = handleCassandraBigintType(colKey, newValuesJson); + assertEquals(Long.MAX_VALUE, value.longValue()); + } + + @Test + public void testHandleMinLong() { + String newValuesString = "{\"age\":-9223372036854775808}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "age"; + Long value = handleCassandraBigintType(colKey, newValuesJson); + assertEquals(Long.MIN_VALUE, value.longValue()); + } + + @Test + public void testHandleMaxFloat() { + String newValuesString = "{\"value\":3.4028235E38}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "value"; + Float value = handleCassandraFloatType(colKey, newValuesJson); + assertEquals(Float.MAX_VALUE, value, 0.01f); + } + + @Test + public void testHandleMinFloat() { + String newValuesString = "{\"value\":-3.4028235E38}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "value"; + Float value = handleCassandraFloatType(colKey, newValuesJson); + assertEquals(-Float.MAX_VALUE, value, 0.01f); + } + + @Test + public void testHandleMaxDouble() { + String newValuesString = "{\"value\":1.7976931348623157E308}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "value"; + Double value = handleCassandraDoubleType(colKey, newValuesJson); + assertEquals(Double.MAX_VALUE, value, 0.01); + } + + @Test + public void testHandleMinDouble() { + String newValuesString = "{\"value\":-1.7976931348623157E308}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "value"; + Double value = handleCassandraDoubleType(colKey, newValuesJson); + assertEquals(-Double.MAX_VALUE, value, 0.01); + } + + @Test + public void testHandleInvalidIntegerFormat() { + String newValuesString = "{\"age\":\"invalid_integer\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "age"; + handleCassandraIntType(colKey, newValuesJson); + } + + @Test + public void testHandleInvalidLongFormat() { + String newValuesString = "{\"age\":\"invalid_long\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "age"; + handleCassandraBigintType(colKey, newValuesJson); + } + + @Test + public void testHandleInvalidFloatFormat() { + String newValuesString = "{\"value\":\"invalid_float\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "value"; + handleCassandraFloatType(colKey, newValuesJson); + } + + @Test + public void testHandleInvalidDoubleFormat() { + String newValuesString = "{\"value\":\"invalid_double\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "value"; + handleCassandraDoubleType(colKey, newValuesJson); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleInvalidBlobFormat() { + String newValuesString = "{\"data\":\"not_base64\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "data"; + handleCassandraBlobType(colKey, newValuesJson); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleInvalidDateFormat() { + String newValuesString = "{\"birthdate\":\"invalid_date_format\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "birthdate"; + handleCassandraDateType(colKey, newValuesJson); + } + + @Test + public void testHandleNullTextType() { + String newValuesString = "{\"name\":null}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "name"; + String value = handleCassandraTextType(colKey, newValuesJson); + assertNull(value); + } + + @Test + public void testHandleUnsupportedBooleanType() { + String newValuesString = "{\"values\":[true, false]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + + expectedEx.expect(IllegalArgumentException.class); + expectedEx.expectMessage("Unsupported type for column values"); + + handleFloatSetType("values", newValuesJson); + } + + @Test + public void testHandleUnsupportedListType() { + String newValuesString = "{\"values\":[[1, 2], [3, 4]]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + + expectedEx.expect(IllegalArgumentException.class); + expectedEx.expectMessage("Unsupported type for column values"); + + handleFloatSetType("values", newValuesJson); + } + + @Test + public void testHandleUnsupportedMapType() { + String newValuesString = "{\"values\":[{\"key1\":\"value1\"}, {\"key2\":\"value2\"}]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + + expectedEx.expect(IllegalArgumentException.class); + expectedEx.expectMessage("Unsupported type for column values"); + + handleFloatSetType("values", newValuesJson); + } + + @Test + public void testHandleUnsupportedType() { + String newValuesString = "{\"values\":[true, false]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + + expectedEx.expect(IllegalArgumentException.class); + expectedEx.expectMessage("Unsupported type for column values"); + + handleFloatSetType("values", newValuesJson); + } + + @Test + public void convertSpannerValueJsonToBlobType_FromBase64() { + String newValuesString = "{\"FirstName\":\"kk\",\"LastName\":\"ll\", \"data\":\"QUJDRA==\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "data"; + ByteBuffer convertedValue = handleCassandraBlobType(colKey, newValuesJson); + byte[] expectedBytes = Base64.getDecoder().decode("QUJDRA=="); + byte[] actualBytes = new byte[convertedValue.remaining()]; + convertedValue.get(actualBytes); + Assert.assertArrayEquals(expectedBytes, actualBytes); + } + + @Test + public void convertSpannerValueJsonToBlobType_EmptyString() { + String newValuesString = "{\"FirstName\":\"kk\",\"LastName\":\"ll\", \"data\":\"\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "data"; + ByteBuffer convertedValue = handleCassandraBlobType(colKey, newValuesJson); + Assert.assertNotNull(convertedValue); + assertEquals(0, convertedValue.remaining()); + } + + @Test(expected = IllegalArgumentException.class) + public void convertSpannerValueJsonToBlobType_InvalidType() { + String newValuesString = "{\"FirstName\":\"kk\",\"LastName\":\"ll\", \"data\":12345}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "data"; + handleCassandraBlobType(colKey, newValuesJson); + } + + @Test + public void convertSpannerValueJsonToInvalidFloatType() { + String newValuesString = + "{\"FirstName\":\"kk\",\"LastName\":\"ll\", \"age\":\"invalid_value\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "age"; + handleCassandraFloatType(colKey, newValuesJson); + } + + @Test + public void convertSpannerValueJsonToInvalidDoubleType() { + String newValuesString = + "{\"FirstName\":\"kk\",\"LastName\":\"ll\", \"salary\":\"invalid_value\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "salary"; + handleCassandraDoubleType(colKey, newValuesJson); + } + + @Test + public void convertSpannerValueJsonToBlobType_MissingColumn() { + String newValuesString = "{\"FirstName\":\"kk\",\"LastName\":\"ll\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "data"; + ByteBuffer convertedValue = handleCassandraBlobType(colKey, newValuesJson); + Assert.assertNull(convertedValue); + } + + @Test + public void testHandleByteArrayType() { + String newValuesString = "{\"data\":[\"QUJDRA==\", \"RkZIRg==\"]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + List value = handleByteArrayType("data", newValuesJson); + + List expected = + Arrays.asList( + ByteBuffer.wrap(Base64.getDecoder().decode("QUJDRA==")), + ByteBuffer.wrap(Base64.getDecoder().decode("RkZIRg=="))); + assertEquals(expected, value); + } + + @Test + public void testHandleByteSetType() { + String newValuesString = "{\"data\":[\"QUJDRA==\", \"RkZIRg==\"]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + Set value = handleByteSetType("data", newValuesJson); + + Set expected = + new HashSet<>( + Arrays.asList( + ByteBuffer.wrap(Base64.getDecoder().decode("QUJDRA==")), + ByteBuffer.wrap(Base64.getDecoder().decode("RkZIRg==")))); + assertEquals(expected, value); + } + + @Test + public void testHandleStringArrayType() { + String newValuesString = "{\"names\":[\"Alice\", \"Bob\", \"Charlie\"]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + List value = handleStringArrayType("names", newValuesJson); + + List expected = Arrays.asList("Alice", "Bob", "Charlie"); + assertEquals(expected, value); + } + + @Test + public void testHandleStringSetType() { + String newValuesString = "{\"names\":[\"Alice\", \"Bob\", \"Alice\", \"Charlie\"]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + Set valueList = handleStringSetType("names", newValuesJson); + HashSet value = new HashSet<>(valueList); + HashSet expected = new HashSet<>(Arrays.asList("Alice", "Bob", "Charlie")); + assertEquals(expected, value); + } + + @Test + public void testHandleBoolSetTypeString() { + String newValuesString = "{\"flags\":[\"true\", \"false\", \"true\"]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + Set value = handleBoolSetTypeString("flags", newValuesJson); + + Set expected = new HashSet<>(Arrays.asList(true, false)); + assertEquals(expected, value); + } + + @Test + public void testHandleFloatArrayType() { + String newValuesString = "{\"values\":[1.1, 2.2, 3.3]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + List value = handleFloatArrayType("values", newValuesJson); + + List expected = Arrays.asList(1.1f, 2.2f, 3.3f); + assertEquals(expected, value); + } + + @Test + public void testHandleFloatSetType() { + String newValuesString = "{\"values\":[1.1, 2.2, 3.3, 2.2]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + Set value = handleFloatSetType("values", newValuesJson); + + Set expected = new HashSet<>(Arrays.asList(1.1f, 2.2f, 3.3f)); + assertEquals(expected, value); + } + + @Test + public void testHandleFloatSetType_InvalidString() { + String newValuesString = "{\"values\":[\"1.1\", \"2.2\", \"abc\"]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + try { + handleFloatSetType("values", newValuesJson); + fail("Expected IllegalArgumentException for invalid number format"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Invalid number format for column values")); + } + } + + @Test + public void testHandleFloat64ArrayType() { + String newValuesString = "{\"values\":[1.1, \"2.2\", 3.3]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + List value = handleFloat64ArrayType("values", newValuesJson); + + List expected = Arrays.asList(1.1, 2.2, 3.3); + assertEquals(expected, value); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleFloat64ArrayTypeInvalid() { + String newValuesString = "{\"values\":[\"1.1\", \"abc\", \"3.3\"]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + handleFloat64ArrayType("values", newValuesJson); + } + + @Test + public void testHandleDateSetType() { + String newValuesString = "{\"dates\":[\"2024-12-05\", \"2024-12-06\"]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + Set value = handleDateSetType("dates", newValuesJson); + Set expected = + new HashSet<>(Arrays.asList(LocalDate.of(2024, 12, 5), LocalDate.of(2024, 12, 6))); + assertEquals(expected, value); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleFloat64ArrayType_WithUnsupportedList() { + String jsonStr = "{\"colName\": [[1, 2, 3], [4, 5, 6]]}"; + JSONObject valuesJson = new JSONObject(jsonStr); + CassandraTypeHandler.handleFloat64ArrayType("colName", valuesJson); + } + + @Test + public void testHandleInt64SetType_ValidLongValues() { + String newValuesString = "{\"numbers\":[1, 2, 3, 4]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + Set result = handleInt64SetType("numbers", newValuesJson); + Set expected = new HashSet<>(Arrays.asList(1L, 2L, 3L, 4L)); + assertEquals(expected, result); + } + + @Test + public void testHandleCassandraIntType_ValidInteger() { + String newValuesString = "{\"age\":1234}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + Integer result = handleCassandraIntType("age", newValuesJson); + Integer expected = 1234; + assertEquals(expected, result); + } + + @Test + public void testHandleCassandraBigintType_ValidConversion() { + String newValuesString = "{\"age\":1234567890123}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + Long result = handleCassandraBigintType("age", newValuesJson); + Long expected = 1234567890123L; + assertEquals(expected, result); + } + + @Test + public void testHandleInt64ArrayAsInt32Array() { + String newValuesString = "{\"values\":[1, 2, 3, 4]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + List value = handleInt64ArrayAsInt32Array("values", newValuesJson); + + List expected = Arrays.asList(1, 2, 3, 4); + assertEquals(expected, value); + } + + @Test + public void testHandleInt64ArrayAsInt32Set() { + String newValuesString = "{\"values\":[1, 2, 3, 2]}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + Set value = handleInt64ArrayAsInt32Set("values", newValuesJson); + + Set expected = new HashSet<>(Arrays.asList(1, 2, 3)); + assertEquals(expected, value); + } + + @Test + public void testHandleCassandraUuidTypeNull() { + String newValuesString = "{\"uuid\":null}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + UUID value = handleCassandraUuidType("uuid", newValuesJson); + Assert.assertNull(value); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleCassandraTimestampInvalidFormat() { + String newValuesString = "{\"createdAt\":\"2024-12-05 10:15:30.123\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + handleCassandraTimestampType("createdAt", newValuesJson); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleCassandraTimestampInvalidFormatColNull() { + String newValuesString = "{\"createdAt\":\"2024-12-05 10:15:30.123\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + handleCassandraTimestampType("timestamp", newValuesJson); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleCassandraDateInvalidFormat() { + String newValuesString = "{\"birthdate\":\"2024/12/05\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + handleCassandraDateType("birthdate", newValuesJson); + } + + @Test + public void testHandleCassandraTextTypeNull() { + String newValuesString = "{\"name\":null}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String value = handleCassandraTextType("name", newValuesJson); + Assert.assertNull(value); + } + + @Test + public void testHandleBoolArrayType_ValidBooleanStrings() { + String jsonStr = "{\"colName\": [\"true\", \"false\", \"true\"]}"; + JSONObject valuesJson = new JSONObject(jsonStr); + List result = CassandraTypeHandler.handleBoolArrayType("colName", valuesJson); + assertEquals(3, result.size()); + assertTrue(result.get(0)); + assertFalse(result.get(1)); + assertTrue(result.get(2)); + } + + @Test + public void testHandleBoolArrayType_InvalidBooleanStrings() { + String jsonStr = "{\"colName\": [\"yes\", \"no\", \"true\"]}"; + JSONObject valuesJson = new JSONObject(jsonStr); + List result = CassandraTypeHandler.handleBoolArrayType("colName", valuesJson); + assertEquals(3, result.size()); + assertFalse(result.get(0)); + assertFalse(result.get(1)); + assertTrue(result.get(2)); + } + + @Test + public void testHandleBoolArrayType_EmptyArray() { + String jsonStr = "{\"colName\": []}"; + JSONObject valuesJson = new JSONObject(jsonStr); + List result = CassandraTypeHandler.handleBoolArrayType("colName", valuesJson); + assertTrue(result.isEmpty()); + } + + @Test + public void testHandleTimestampSetType_validArray() { + String jsonString = + "{\"timestamps\": [\"2024-12-04T12:34:56.123Z\", \"2024-12-05T13:45:00.000Z\"]}"; + JSONObject valuesJson = new JSONObject(jsonString); + + Set result = CassandraTypeHandler.handleTimestampSetType("timestamps", valuesJson); + + assertNotNull(result); + assertEquals(2, result.size()); + assertTrue(result.contains(Timestamp.valueOf("2024-12-04 00:00:00.0"))); + assertTrue(result.contains(Timestamp.valueOf("2024-12-05 00:00:00.0"))); + } + + @Test + public void testHandleValidAsciiString() { + String newValuesString = "{\"name\":\"JohnDoe\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "name"; + assertEquals("JohnDoe", handleCassandraAsciiType(colKey, newValuesJson)); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleNonAsciiString() { + String newValuesString = "{\"name\":\"JoĆ£oDoe\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "name"; + handleCassandraAsciiType(colKey, newValuesJson); + } + + @Test + public void testHandleNullForAsciiColumn() { + String newValuesString = "{\"name\":null}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "name"; + handleCassandraAsciiType(colKey, newValuesJson); + } + + @Test + public void testHandleValidStringVarint() { + String newValuesString = "{\"amount\":\"123456789123456789\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "amount"; + BigInteger expected = new BigInteger("123456789123456789"); + assertEquals(expected, handleCassandraVarintType(colKey, newValuesJson)); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleInvalidStringVarint() { + String newValuesString = "{\"amount\":\"abcxyz\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "amount"; + handleCassandraVarintType(colKey, newValuesJson); + } + + @Test + public void testHandleInvalidTypeVarint() { + String newValuesString = "{\"amount\":12345}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "amount"; + handleCassandraVarintType(colKey, newValuesJson); + } + + @Test + public void testHandleValidDuration() { + String newValuesString = "{\"duration\":\"P1DT1H\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "duration"; + Duration expected = Duration.parse("P1DT1H"); + assertEquals(expected, handleCassandraDurationType(colKey, newValuesJson)); + } + + @Test + public void testHandleNullDuration() { + String newValuesString = "{\"duration\":null}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "duration"; + assertNull(handleCassandraDurationType(colKey, newValuesJson)); + } + + @Test + public void testHandleMissingColumnKey() { + String newValuesString = "{\"otherColumn\":\"P1DT1H\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "duration"; + assertNull(handleCassandraDurationType(colKey, newValuesJson)); + } + + @Test + public void testHandleValidIPv4Address() throws UnknownHostException { + String newValuesString = "{\"ipAddress\":\"192.168.0.1\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "ipAddress"; + InetAddress expected = InetAddress.getByName("192.168.0.1"); + assertEquals(expected, handleCassandraInetAddressType(colKey, newValuesJson)); + } + + @Test + public void testHandleValidIPv6Address() throws Exception { + String newValuesString = "{\"ipAddress\":\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "ipAddress"; + InetAddress actual = CassandraTypeHandler.handleCassandraInetAddressType(colKey, newValuesJson); + InetAddress expected = InetAddress.getByName("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); + assertEquals(expected, actual); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleInvalidIPAddressFormat() throws IllegalArgumentException { + String newValuesString = "{\"ipAddress\":\"invalid-ip-address\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "ipAddress"; + handleCassandraInetAddressType(colKey, newValuesJson); + } + + @Test + public void testHandleEmptyStringIPAddress() { + String newValuesString = "{\"ipAddress\":\"192.168.1.1\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "ipAddress"; + Object result = handleCassandraInetAddressType(colKey, newValuesJson); + assertTrue("Expected result to be of type InetAddress", result instanceof InetAddress); + assertEquals( + "IP address does not match", "192.168.1.1", ((InetAddress) result).getHostAddress()); + } + + @Test + public void testHandleStringifiedJsonToMapWithEmptyJson() { + String newValuesString = "{}"; + JSONObject newValuesJson = new JSONObject(); + newValuesJson.put("data", newValuesString); + String colKey = "data"; + Map expected = Map.of(); + Map result = handleStringifiedJsonToMap(colKey, newValuesJson); + assertEquals(expected, result); + } + + @Test + public void testHandleStringifiedJsonToMapWithSimpleJson() { + String newValuesString = "{\"name\":\"John\", \"age\":30}"; + JSONObject newValuesJson = new JSONObject(); + newValuesJson.put("data", newValuesString); + String colKey = "data"; + Map expected = Map.of("name", "John", "age", 30); + Map result = handleStringifiedJsonToMap(colKey, newValuesJson); + assertEquals(expected, result); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleStringifiedJsonToMapWithInvalidJson() { + String newValuesString = "{\"user\":{\"name\":\"John\", \"age\":30"; + JSONObject newValuesJson = new JSONObject(); + newValuesJson.put("data", newValuesString); + String colKey = "data"; + handleStringifiedJsonToMap(colKey, newValuesJson); + } + + @Test + public void testHandleStringifiedJsonToMapWithNullValues() { + String newValuesString = "{\"name\":null, \"age\":null}"; + JSONObject newValuesJson = new JSONObject(); + newValuesJson.put("data", newValuesString); + String colKey = "data"; + Map expected = + Map.of( + "name", JSONObject.NULL, + "age", JSONObject.NULL); + Map result = handleStringifiedJsonToMap(colKey, newValuesJson); + assertEquals(expected, result); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleInvalidStringifiedJson() { + String newValuesString = "{\"user\":{\"name\":\"John\", \"age\":30"; + JSONObject newValuesJson = new JSONObject(); + newValuesJson.put("data", newValuesString); + String colKey = "data"; + handleStringifiedJsonToMap(colKey, newValuesJson); + } + + @Test(expected = IllegalArgumentException.class) + public void testHandleNonStringValue() { + JSONObject newValuesJson = new JSONObject(); + newValuesJson.put("data", 12345); + String colKey = "data"; + handleStringifiedJsonToMap(colKey, newValuesJson); + } + + @Test + public void testHandleValidStringifiedJsonArray() { + String newValuesString = "[\"apple\", \"banana\", \"cherry\"]"; + JSONObject newValuesJson = new JSONObject(); + newValuesJson.put("data", newValuesString); + String colKey = "data"; + + Set expected = new HashSet<>(); + expected.add("apple"); + expected.add("banana"); + expected.add("cherry"); + assertEquals(expected, handleStringifiedJsonToSet(colKey, newValuesJson)); + } + + @Test + public void testHandleEmptyStringifiedJsonArray() { + String newValuesString = "[]"; + JSONObject newValuesJson = new JSONObject(); + newValuesJson.put("data", newValuesString); + String colKey = "data"; + Set expected = new HashSet<>(); + assertEquals(expected, handleStringifiedJsonToSet(colKey, newValuesJson)); + } + + @Test + public void testHandleNonArrayValue() { + String newValuesString = "\"apple\""; + JSONObject newValuesJson = new JSONObject(); + newValuesJson.put("data", newValuesString); + String colKey = "data"; + assertThrows( + IllegalArgumentException.class, () -> handleStringifiedJsonToSet(colKey, newValuesJson)); + } + + @Test + public void testConvertToSmallIntValidInput() { + Integer validValue = 100; + short result = convertToSmallInt(validValue); + assertEquals(100, result); + } + + @Test + public void testConvertToSmallIntBelowMinValue() { + Integer invalidValue = Short.MIN_VALUE - 1; + assertThrows(IllegalArgumentException.class, () -> convertToSmallInt(invalidValue)); + } + + @Test + public void testConvertToSmallIntAboveMaxValue() { + Integer invalidValue = Short.MAX_VALUE + 1; + assertThrows(IllegalArgumentException.class, () -> convertToSmallInt(invalidValue)); + } + + @Test + public void testConvertToTinyIntValidInput() { + Integer validValue = 100; + byte result = convertToTinyInt(validValue); + assertEquals(100, result); + } + + @Test + public void testConvertToTinyIntBelowMinValue() { + Integer invalidValue = Byte.MIN_VALUE - 1; + assertThrows(IllegalArgumentException.class, () -> convertToTinyInt(invalidValue)); + } + + @Test + public void testConvertToTinyIntAboveMaxValue() { + Integer invalidValue = Byte.MAX_VALUE + 1; + assertThrows(IllegalArgumentException.class, () -> convertToTinyInt(invalidValue)); + } + + @Test + public void testEscapeCassandraStringNoQuotes() { + String input = "Hello World"; + String expected = "Hello World"; + String result = escapeCassandraString(input); + assertEquals(expected, result); + } + + @Test + public void testEscapeCassandraStringWithSingleQuote() { + String input = "O'Reilly"; + String expected = "O''Reilly"; + String result = escapeCassandraString(input); + assertEquals(expected, result); + } + + @Test + public void testEscapeCassandraStringEmpty() { + String input = ""; + String expected = ""; + String result = escapeCassandraString(input); + assertEquals(expected, result); + } + + @Test + public void testEscapeCassandraStringWithMultipleQuotes() { + String input = "It's John's book."; + String expected = "It''s John''s book."; + String result = escapeCassandraString(input); + assertEquals(expected, result); + } + + @Test + public void testConvertToCassandraTimestampWithValidOffset() { + String value = "2024-12-12T10:15:30+02:00"; + String timezoneOffset = "+00:00"; + String expected = "'2024-12-12T08:15:30Z'"; + String result = convertToCassandraTimestamp(value, timezoneOffset); + assertEquals(expected, result); + } + + @Test + public void testConvertToCassandraTimestampWithNonZeroOffset() { + String value = "2024-12-12T10:15:30+02:00"; + String timezoneOffset = "+00:00"; + String expected = "'2024-12-12T08:15:30Z'"; + String result = convertToCassandraTimestamp(value, timezoneOffset); + assertEquals(expected, result); + } + + @Test + public void testConvertToCassandraTimestampWithNegativeOffset() { + String value = "2024-12-12T10:15:30-05:00"; + String timezoneOffset = "+00:00"; + String expected = "'2024-12-12T15:15:30Z'"; + String result = convertToCassandraTimestamp(value, timezoneOffset); + assertEquals(expected, result); + } + + @Test(expected = RuntimeException.class) + public void testConvertToCassandraTimestampWithInvalidFormat() { + String value = "2024-12-12T25:15:30+02:00"; + String timezoneOffset = "+00:00"; + convertToCassandraTimestamp(value, timezoneOffset); + } + + @Test + public void testConvertToCassandraTimestampWithoutTimezone() { + String value = "2024-12-12T10:15:30Z"; + String timezoneOffset = "+00:00"; + String expected = "'2024-12-12T10:15:30Z'"; + String result = convertToCassandraTimestamp(value, timezoneOffset); + assertEquals(expected, result); + } + + @Test + public void testConvertToCassandraDateWithValidDate() { + String dateString = "2024-12-12T10:15:30Z"; + LocalDate result = convertToCassandraDate(dateString); + LocalDate expected = LocalDate.of(2024, 12, 12); + assertEquals(expected, result); + } + + @Test + public void testConvertToCassandraDateLeapYear() { + String dateString = "2024-02-29T00:00:00Z"; + LocalDate result = convertToCassandraDate(dateString); + LocalDate expected = LocalDate.of(2024, 2, 29); + assertEquals(expected, result); + } + + @Test + public void testConvertToCassandraDateWithDifferentTimeZone() { + String dateString = "2024-12-12T10:15:30+02:00"; + LocalDate result = convertToCassandraDate(dateString); + LocalDate expected = LocalDate.of(2024, 12, 12); + assertEquals(expected, result); + } + + @Test(expected = IllegalArgumentException.class) + public void testConvertToCassandraDateWithInvalidDate() { + String dateString = "2024-13-12T10:15:30Z"; + convertToCassandraDate(dateString); + } + + @Test + public void testConvertToCassandraTimestampWithValidDate() { + String dateString = "2024-12-12T10:15:30Z"; + Instant result = convertToCassandraTimestamp(dateString); + Instant expected = Instant.parse(dateString); + assertEquals(expected, result); + } + + @Test + public void testConvertToCassandraTimestampWithTimezoneOffset() { + String dateString = "2024-12-12T10:15:30+02:00"; + Instant result = convertToCassandraTimestamp(dateString); + Instant expected = Instant.parse("2024-12-12T08:15:30Z"); + assertEquals(expected, result); + } + + @Test + public void testConvertToCassandraTimestampLeapYear() { + String dateString = "2024-02-29T00:00:00Z"; + Instant result = convertToCassandraTimestamp(dateString); + Instant expected = Instant.parse(dateString); + assertEquals(expected, result); + } + + @Test(expected = IllegalArgumentException.class) + public void testConvertToCassandraTimestampWithInvalidDate() { + String dateString = "2024-13-12T10:15:30Z"; + convertToCassandraTimestamp(dateString); + } + + @Test + public void testIsValidUUIDWithValidUUID() { + String validUUID = "123e4567-e89b-12d3-a456-426614174000"; + boolean result = isValidUUID(validUUID); + assertTrue(result); + } + + @Test + public void testIsValidUUIDWithInvalidUUID() { + String invalidUUID = "123e4567-e89b-12d3-a456-426614174000Z"; + boolean result = isValidUUID(invalidUUID); + assertFalse(result); + } + + @Test + public void testIsValidUUIDWithEmptyString() { + String emptyString = ""; + boolean result = isValidUUID(emptyString); + assertFalse(result); + } + + @Test + public void testIsValidIPAddressWithValidIPv4() { + String validIPv4 = "192.168.1.1"; + boolean result = isValidIPAddress(validIPv4); + assertTrue(result); + } + + @Test + public void testIsValidIPAddressWithValidIPv6() { + String validIPv6 = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + boolean result = isValidIPAddress(validIPv6); + assertTrue(result); + } + + @Test + public void testIsValidIPAddressWithInvalidFormat() { + String invalidIP = "999.999.999.999"; + boolean result = isValidIPAddress(invalidIP); + assertFalse(result); + } + + @Test + public void testIsValidJSONWithValidJSON() { + String validJson = "{\"name\":\"John\", \"age\":30}"; + boolean result = isValidJSON(validJson); + assertTrue(result); + } + + @Test + public void testIsValidJSONWithInvalidJSON() { + String invalidJson = "{\"name\":\"John\", \"age\":30"; + boolean result = isValidJSON(invalidJson); + assertFalse(result); + } + + @Test + public void testIsValidJSONWithEmptyString() { + String emptyString = ""; + boolean result = isValidJSON(emptyString); + assertFalse(result); + } + + @Test + public void testIsValidJSONWithNull() { + String nullString = null; + boolean result = isValidJSON(nullString); + assertFalse(result); + } + + @Test + public void testConvertToCassandraDate_validDateString() { + String dateString = "2024-12-16T14:30:00Z"; + LocalDate result = CassandraTypeHandler.convertToCassandraDate(dateString); + assertEquals("The parsed LocalDate should be '2024-12-16'", LocalDate.of(2024, 12, 16), result); + } + + @Test + public void testConvertToCassandraDate_leapYear() { + String dateString = "2024-02-29T00:00:00Z"; + LocalDate result = CassandraTypeHandler.convertToCassandraDate(dateString); + assertEquals("The parsed LocalDate should be '2024-02-29'", LocalDate.of(2024, 2, 29), result); + } + + @Test + public void testConvertToCassandraDate_validDateWithMilliseconds() { + String dateString = "2024-12-16T14:30:00.123Z"; + LocalDate result = CassandraTypeHandler.convertToCassandraDate(dateString); + assertEquals("The parsed LocalDate should be '2024-12-16'", LocalDate.of(2024, 12, 16), result); + } + + @Test + public void testConvertToCassandraDate_timezoneOffsetImpact() { + String dateString = "2024-12-16T14:30:00+01:00"; + LocalDate result = CassandraTypeHandler.convertToCassandraDate(dateString); + assertEquals( + "The parsed LocalDate should be '2024-12-16' regardless of timezone.", + LocalDate.of(2024, 12, 16), + result); + } + + @Test + public void testConvertToCassandraDate_validDateWithOffset() { + String dateString = "2024-12-16T14:30:00+01:00"; + LocalDate result = CassandraTypeHandler.convertToCassandraDate(dateString); + assertEquals("The parsed LocalDate should be '2024-12-16'", LocalDate.of(2024, 12, 16), result); + } + + @Test + public void testConvertToCassandraDate_withTimeZoneOffset() { + String validDateWithOffset = "2024-12-16T14:30:00+02:00"; + LocalDate result = CassandraTypeHandler.convertToCassandraDate(validDateWithOffset); + assertNotNull(String.valueOf(result), "The result should not be null"); + assertEquals( + "The parsed LocalDate should match the expected value (timezone offset ignored).", + LocalDate.of(2024, 12, 16), + result); + } + + @Test + public void testConvertToCassandraDate_endOfMonth() { + String endOfMonthDate = "2024-01-31T12:00:00Z"; + LocalDate result = CassandraTypeHandler.convertToCassandraDate(endOfMonthDate); + assertNotNull(String.valueOf(result), "The result should not be null"); + assertEquals( + "The parsed LocalDate should be correct for end of month.", + LocalDate.of(2024, 1, 31), + result); + } + + @Test + public void testParseDate_validStringWithCustomFormatter() { + String dateStr = "2024-12-16T14:30:00.000"; + String formatter = "yyyy-MM-dd'T'HH:mm:ss.SSS"; + String colName = "testDate"; + + LocalDate result = CassandraTypeHandler.parseDate(colName, dateStr, formatter); + + assertNotNull(String.valueOf(result), "The parsed LocalDate should not be null."); + assertEquals( + "The parsed LocalDate should match the expected value.", + LocalDate.of(2024, 12, 16), + result); + } + + @Test + public void testParseDate_validString() { + String validDateStr = "2024-12-16T14:30:00.000+0000"; + String formatter = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; + String colName = "testDate"; + LocalDate result = CassandraTypeHandler.parseDate(colName, validDateStr, formatter); + assertNotNull(result); + assertEquals(LocalDate.of(2024, 12, 16), result); + } + + @Test + public void testParseDate_validDate() { + Date date = new Date(1700000000000L); + String colName = "testDate"; + + LocalDate result = CassandraTypeHandler.parseDate(colName, date, "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + + assertNotNull(result); + assertNotEquals(LocalDate.of(2024, 12, 15), result); + } + + @Test + public void testHandleCassandraGenericDateType_NullFormatter() { + String newValuesString = "{\"date\":\"2024-12-16T10:15:30.000+0000\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "date"; + LocalDate result = + CassandraTypeHandler.handleCassandraGenericDateType(colKey, newValuesJson, null); + assertEquals(LocalDate.of(2024, 12, 16), result); + } + + @Test + public void testHandleStringifiedJsonToList_InvalidFormat() { + String newValuesString = "{\"column\": \"{\\\"key\\\":\\\"value\\\"}\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "column"; + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> { + CassandraTypeHandler.handleStringifiedJsonToList(colKey, newValuesJson); + }); + assertTrue(thrown.getMessage().contains("Invalid stringified JSON array format")); + } + + @Test + public void testHandleStringifiedJsonToList_NullInput() { + JSONObject newValuesJson = null; + String colKey = "column"; + assertThrows( + NullPointerException.class, + () -> { + CassandraTypeHandler.handleStringifiedJsonToList(colKey, newValuesJson); + }); + } + + @Test + public void testHandleStringifiedJsonToMap_EmptyString() { + // Test case with an empty string as input, which is also an invalid JSON format + String newValuesString = "{\"column\": \"\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "column"; + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> { + CassandraTypeHandler.handleStringifiedJsonToMap(colKey, newValuesJson); + }); + assertTrue(thrown.getMessage().contains("Invalid stringified JSON format for column")); + } + + @Test + public void testHandleStringifiedJsonToMap_NonJsonString() { + String newValuesString = "{\"column\": \"just a plain string\"}"; + JSONObject newValuesJson = new JSONObject(newValuesString); + String colKey = "column"; + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> { + CassandraTypeHandler.handleStringifiedJsonToMap(colKey, newValuesJson); + }); + assertTrue(thrown.getMessage().contains("Invalid stringified JSON format for column")); + } + + @Test + public void testHandleCassandraVarintType_ValidByteArray() { + JSONObject valuesJson = new JSONObject(); + byte[] byteArray = new BigInteger("12345678901234567890").toByteArray(); + valuesJson.put("varint", byteArray); + BigInteger result = CassandraTypeHandler.handleCassandraVarintType("varint", valuesJson); + BigInteger expected = new BigInteger(byteArray); + assertEquals(expected, result); + } + + @Test + public void testHandleCassandraVarintType_InvalidStringFormat() { + JSONObject valuesJson = new JSONObject(); + valuesJson.put("col1", "invalid-number"); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + handleCassandraVarintType("col1", valuesJson); + }); + assertTrue(exception.getMessage().contains("Invalid varint format (string) for column: col1")); + } + + @Test + public void testParseDate_UnsupportedType() { + JSONObject valuesJson = new JSONObject(); + valuesJson.put("col1", 12345); + String formatter = "yyyy-MM-dd"; + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + CassandraTypeHandler.parseDate("col1", valuesJson.get("col1"), formatter); + }); + assertTrue(exception.getMessage().contains("Unsupported type for column col1")); + } + + @Test + public void testHandleCassandraUuidType_ValidUuidString() { + JSONObject valuesJson = new JSONObject(); + String validUuidString = "123e4567-e89b-12d3-a456-426614174000"; + valuesJson.put("col1", validUuidString); + UUID result = handleCassandraUuidType("col1", valuesJson); + UUID expectedUuid = UUID.fromString(validUuidString); + assertEquals(expectedUuid, result); + } + + @Test + public void testHandleCassandraInetAddressType_Hostname() { + JSONObject valuesJson = new JSONObject(); + valuesJson.put("col1", "www.google.com"); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + CassandraTypeHandler.handleCassandraInetAddressType("col1", valuesJson); + }); + assertTrue(exception.getMessage().contains("Invalid IP address format for column: col1")); + } +}