@@ -1637,6 +1637,80 @@ public void testOpenSearchIndexWithInvalidChars() throws IOException, Interrupte
16371637 Assert .assertThrows (RuntimeException .class , () -> sink .doInitialize ());
16381638 }
16391639
1640+ @ Test
1641+ @ DisabledIf (value = "isDataStreamNotSupported" , disabledReason = "Data streams require OpenSearch 1.3.0+" )
1642+ public void testDataStreamDetection () throws IOException , InterruptedException {
1643+ final String dataStreamName = "test-data-stream-" + UUID .randomUUID ();
1644+ final String templateName = dataStreamName + "-template" ;
1645+ final File tempDirectory = Files .createTempDirectory ("" ).toFile ();
1646+ final String dlqFile = tempDirectory .getAbsolutePath () + "/test-dlq.txt" ;
1647+
1648+ try {
1649+ // Create an index template for the data stream first
1650+ final Request createTemplateRequest = new Request (HttpMethod .PUT , "/_index_template/" + templateName );
1651+ final String templateBody = "{" +
1652+ "\" index_patterns\" : [\" " + dataStreamName + "\" ]," +
1653+ "\" data_stream\" : {}," +
1654+ "\" template\" : {" +
1655+ "\" mappings\" : {" +
1656+ "\" properties\" : {" +
1657+ "\" @timestamp\" : {\" type\" : \" date\" }" +
1658+ "}" +
1659+ "}" +
1660+ "}" +
1661+ "}" ;
1662+ createTemplateRequest .setJsonEntity (templateBody );
1663+ client .performRequest (createTemplateRequest );
1664+
1665+ // Create a data stream
1666+ final Request createDataStreamRequest = new Request (HttpMethod .PUT , "/_data_stream/" + dataStreamName );
1667+ client .performRequest (createDataStreamRequest );
1668+
1669+ // Initialize sink AFTER creating the data stream so detection works
1670+ Map <String , Object > metadata = initializeConfigurationMetadata (null , dataStreamName , null );
1671+ metadata .put (RetryConfiguration .DLQ_FILE , dlqFile );
1672+ final OpenSearchSinkConfig openSearchSinkConfig = generateOpenSearchSinkConfigByMetadata (metadata );
1673+ final OpenSearchSink sink = createObjectUnderTest (openSearchSinkConfig , true );
1674+
1675+ // Test that the data stream is detected
1676+ final String testIdField = "someId" ;
1677+ final String testId = "foo" ;
1678+ final List <Record <Event >> testRecords = Collections .singletonList (jsonStringToRecord (generateCustomRecordJson (testIdField , testId )));
1679+
1680+ sink .output (testRecords );
1681+ sink .shutdown ();
1682+
1683+ // Wait for indexing to complete
1684+ Thread .sleep (2000 );
1685+
1686+ // Verify the document was written to the data stream
1687+ final List <Map <String , Object >> retSources = getSearchResponseDocSources (dataStreamName );
1688+ assertThat ("Expected 1 document in data stream " + dataStreamName + " but found " + retSources .size (),
1689+ retSources .size (), equalTo (1 ));
1690+ } catch (Exception e ) {
1691+ throw e ;
1692+ } finally {
1693+ // Clean up the data stream
1694+ final Request deleteDataStreamRequest = new Request (HttpMethod .DELETE , "/_data_stream/" + dataStreamName );
1695+ try {
1696+ client .performRequest (deleteDataStreamRequest );
1697+ } catch (IOException e ) {
1698+ // Ignore cleanup errors
1699+ }
1700+
1701+ // Clean up the index template
1702+ final Request deleteTemplateRequest = new Request (HttpMethod .DELETE , "/_index_template/" + templateName );
1703+ try {
1704+ client .performRequest (deleteTemplateRequest );
1705+ } catch (IOException e ) {
1706+ // Ignore cleanup errors
1707+ }
1708+
1709+ // Clean up DLQ
1710+ FileUtils .deleteQuietly (tempDirectory );
1711+ }
1712+ }
1713+
16401714 @ Test
16411715 @ Timeout (value = 1 , unit = TimeUnit .MINUTES )
16421716 @ DisabledIf (value = "isES6" ,
@@ -1962,6 +2036,96 @@ private static boolean isES6() {
19622036 return DeclaredOpenSearchVersion .OPENDISTRO_0_10 .compareTo (OpenSearchIntegrationHelper .getVersion ()) >= 0 ;
19632037 }
19642038
2039+ private static boolean isDataStreamNotSupported () {
2040+ // Data streams require OpenSearch 1.3.0+
2041+ return OpenSearchIntegrationHelper .getVersion ().compareTo (DeclaredOpenSearchVersion .parse ("opensearch:1.3.0" )) < 0 ;
2042+ }
2043+
2044+ @ Test
2045+ @ DisabledIf (value = "isDataStreamNotSupported" , disabledReason = "Data streams require OpenSearch 1.3.0+" )
2046+ public void testDataStreamFirstWriteWinsBehavior () throws IOException , InterruptedException {
2047+ final String dataStreamName = "test-first-write-wins-" + UUID .randomUUID ();
2048+ final String templateName = dataStreamName + "-template" ;
2049+ final File tempDirectory = Files .createTempDirectory ("" ).toFile ();
2050+ final String dlqFile = tempDirectory .getAbsolutePath () + "/test-dlq.txt" ;
2051+
2052+ try {
2053+ // Create an index template for the data stream
2054+ final Request createTemplateRequest = new Request (HttpMethod .PUT , "/_index_template/" + templateName );
2055+ final String templateBody = "{" +
2056+ "\" index_patterns\" : [\" " + dataStreamName + "\" ]," +
2057+ "\" data_stream\" : {}," +
2058+ "\" template\" : {" +
2059+ "\" mappings\" : {" +
2060+ "\" properties\" : {" +
2061+ "\" @timestamp\" : {\" type\" : \" date\" }," +
2062+ "\" value\" : {\" type\" : \" keyword\" }" +
2063+ "}" +
2064+ "}" +
2065+ "}" +
2066+ "}" ;
2067+ createTemplateRequest .setJsonEntity (templateBody );
2068+ client .performRequest (createTemplateRequest );
2069+
2070+ // Create the data stream
2071+ final Request createDataStreamRequest = new Request (HttpMethod .PUT , "/_data_stream/" + dataStreamName );
2072+ client .performRequest (createDataStreamRequest );
2073+
2074+ // Initialize sink with document_id configuration
2075+ final String testIdField = "someId" ;
2076+ final String testId = "duplicate-id" ;
2077+ Map <String , Object > metadata = initializeConfigurationMetadata (null , dataStreamName , null );
2078+ metadata .put (IndexConfiguration .DOCUMENT_ID_FIELD , testIdField );
2079+ metadata .put (RetryConfiguration .DLQ_FILE , dlqFile );
2080+ final OpenSearchSinkConfig openSearchSinkConfig = generateOpenSearchSinkConfigByMetadata (metadata );
2081+ final OpenSearchSink sink = createObjectUnderTest (openSearchSinkConfig , true );
2082+
2083+ // Write first document with value "first"
2084+ final String firstDoc = "{\" " + testIdField + "\" : \" " + testId + "\" , \" value\" : \" first\" }" ;
2085+ final List <Record <Event >> firstRecords = Collections .singletonList (jsonStringToRecord (firstDoc ));
2086+ sink .output (firstRecords );
2087+
2088+ // Wait for indexing
2089+ Thread .sleep (1000 );
2090+
2091+ // Write second document with same ID but value "second"
2092+ final String secondDoc = "{\" " + testIdField + "\" : \" " + testId + "\" , \" value\" : \" second\" }" ;
2093+ final List <Record <Event >> secondRecords = Collections .singletonList (jsonStringToRecord (secondDoc ));
2094+ sink .output (secondRecords );
2095+
2096+ sink .shutdown ();
2097+
2098+ // Wait for indexing to complete
2099+ Thread .sleep (2000 );
2100+
2101+ // Verify only one document exists
2102+ final List <Map <String , Object >> retSources = getSearchResponseDocSources (dataStreamName );
2103+ assertThat ("Expected exactly 1 document due to first-write-wins" , retSources .size (), equalTo (1 ));
2104+
2105+ // Verify the document has the FIRST value (first-write-wins)
2106+ final Map <String , Object > document = retSources .get (0 );
2107+ assertThat ("Expected first write to win" , document .get ("value" ), equalTo ("first" ));
2108+
2109+ } finally {
2110+ // Clean up
2111+ final Request deleteDataStreamRequest = new Request (HttpMethod .DELETE , "/_data_stream/" + dataStreamName );
2112+ try {
2113+ client .performRequest (deleteDataStreamRequest );
2114+ } catch (IOException e ) {
2115+ // Ignore cleanup errors
2116+ }
2117+
2118+ final Request deleteTemplateRequest = new Request (HttpMethod .DELETE , "/_index_template/" + templateName );
2119+ try {
2120+ client .performRequest (deleteTemplateRequest );
2121+ } catch (IOException e ) {
2122+ // Ignore cleanup errors
2123+ }
2124+
2125+ FileUtils .deleteQuietly (tempDirectory );
2126+ }
2127+ }
2128+
19652129 private static Stream <Object > getAttributeTestSpecialAndExtremeValues () {
19662130 return Stream .of (
19672131 null ,
0 commit comments