1515package com .starrocks .service .arrow .flight .sql ;
1616
1717import com .google .common .cache .Cache ;
18+ import com .google .protobuf .Any ;
1819import com .google .protobuf .ByteString ;
1920import com .starrocks .common .Config ;
2021import com .starrocks .common .DdlException ;
21- import com .starrocks .common .util .ArrowUtil ;
2222import com .starrocks .metric .LongCounterMetric ;
2323import com .starrocks .metric .MetricRepo ;
2424import com .starrocks .plugin .AuditEvent ;
25+ import com .starrocks .qe .ConnectContext ;
2526import com .starrocks .qe .GlobalVariable ;
2627import com .starrocks .qe .QueryState ;
2728import com .starrocks .qe .SessionVariable ;
5657import org .apache .arrow .flight .sql .FlightSqlProducer ;
5758import org .apache .arrow .flight .sql .impl .FlightSql ;
5859import org .apache .arrow .vector .VectorSchemaRoot ;
60+ import org .apache .arrow .vector .ipc .ReadChannel ;
61+ import org .apache .arrow .vector .ipc .message .MessageSerializer ;
5962import org .apache .arrow .vector .types .pojo .Schema ;
6063import org .junit .jupiter .api .BeforeEach ;
6164import org .junit .jupiter .api .Test ;
6265import org .mockito .ArgumentCaptor ;
6366import org .mockito .MockedStatic ;
6467
68+ import java .io .ByteArrayInputStream ;
69+ import java .io .IOException ;
6570import java .lang .reflect .Field ;
66- import java .nio . ByteBuffer ;
67- import java .nio .charset . StandardCharsets ;
71+ import java .lang . reflect . Method ;
72+ import java .nio .channels . Channels ;
6873import java .util .HashMap ;
6974import java .util .Map ;
75+ import java .util .concurrent .CountDownLatch ;
76+ import java .util .concurrent .TimeUnit ;
7077
7178import static org .junit .jupiter .api .Assertions .assertEquals ;
7279import static org .junit .jupiter .api .Assertions .assertNotNull ;
7380import static org .junit .jupiter .api .Assertions .assertNull ;
81+ import static org .junit .jupiter .api .Assertions .assertSame ;
7482import static org .junit .jupiter .api .Assertions .assertThrows ;
7583import static org .junit .jupiter .api .Assertions .assertTrue ;
76- import static org .mockito .Answers .CALLS_REAL_METHODS ;
7784import static org .mockito .ArgumentMatchers .any ;
7885import static org .mockito .ArgumentMatchers .anyBoolean ;
7986import static org .mockito .ArgumentMatchers .anyLong ;
@@ -93,6 +100,7 @@ public class ArrowFlightSqlServiceImplTest {
93100 private ArrowFlightSqlSessionManager sessionManager ;
94101 private ArrowFlightSqlConnectContext mockContext ;
95102 private FlightProducer .CallContext mockCallContext ;
103+ private SessionVariable mockSessionVariable ;
96104 @ Mocked
97105 AuditEncryptionChecker mockChecker ;
98106
@@ -116,6 +124,34 @@ protected boolean isProxyEnabled() {
116124 }
117125 }
118126
127+ private static class CapturingResultListener implements FlightProducer .StreamListener <Result > {
128+ private final CountDownLatch finished = new CountDownLatch (1 );
129+ private volatile Result result ;
130+ private volatile Throwable error ;
131+ private volatile boolean completed ;
132+
133+ @ Override
134+ public void onNext (Result val ) {
135+ this .result = val ;
136+ }
137+
138+ @ Override
139+ public void onError (Throwable t ) {
140+ this .error = t ;
141+ finished .countDown ();
142+ }
143+
144+ @ Override
145+ public void onCompleted () {
146+ this .completed = true ;
147+ finished .countDown ();
148+ }
149+
150+ public boolean await () throws InterruptedException {
151+ return finished .await (5 , TimeUnit .SECONDS );
152+ }
153+ }
154+
119155 @ BeforeEach
120156 public void setUp () throws IllegalAccessException , NoSuchFieldException , InterruptedException {
121157 sessionManager = mock (ArrowFlightSqlSessionManager .class );
@@ -129,7 +165,7 @@ public void setUp() throws IllegalAccessException, NoSuchFieldException, Interru
129165 when (mockContext .getExecutionId ()).thenReturn (new com .starrocks .thrift .TUniqueId (1 , 1 ));
130166 when (mockContext .getArrowFlightSqlToken ()).thenReturn ("token123" );
131167
132- SessionVariable mockSessionVariable = mock (SessionVariable .class );
168+ mockSessionVariable = mock (SessionVariable .class );
133169 when (mockContext .getSessionVariable ()).thenReturn (mockSessionVariable );
134170 when (mockContext .acquireRunningToken (anyLong ())).thenReturn (true );
135171 when (mockSessionVariable .getQueryTimeoutS ()).thenReturn (10 );
@@ -519,42 +555,75 @@ public void testGetStreamSqlInfo() {
519555 }
520556
521557 @ Test
522- public void testCreatePreparedStatement () throws Exception {
523- // mock request
558+ public void testCreatePreparedStatementUsesAnalyzedSchema () throws Exception {
559+ String query = "SELECT 1 AS c1, 'x' AS c2" ;
524560 FlightSql .ActionCreatePreparedStatementRequest request = mock (FlightSql .ActionCreatePreparedStatementRequest .class );
525- when (request .getQuery ()).thenReturn ("SELECT 1" );
561+ when (request .getQuery ()).thenReturn (query );
562+ ArrowFlightSqlConnectContext realContext = new ArrowFlightSqlConnectContext ("token123" );
563+ when (sessionManager .validateAndGetConnectContext ("token123" )).thenReturn (realContext );
526564
527- // mock context
528- FlightProducer .CallContext callContext = mock (FlightProducer .CallContext .class );
529- when (callContext .peerIdentity ()).thenReturn ("token123" );
565+ CapturingResultListener listener = new CapturingResultListener ();
566+ service .createPreparedStatement (request , mockCallContext , listener );
530567
531- // mock sessionManager
532- when (sessionManager .validateAndGetConnectContext ("token123" )).thenReturn (mockContext );
568+ FlightSql .ActionCreatePreparedStatementResult preparedStatementResult = awaitPreparedStatementResult (listener );
533569
534- // mock listener
535- FlightProducer .StreamListener <Result > listener = mock (FlightProducer .StreamListener .class );
536-
537- // mock ArrowUtil.createSingleSchemaRoot
538- try (
539- MockedStatic <ArrowUtil > mockArrowUtil = mockStatic (ArrowUtil .class );
540- MockedStatic <ArrowFlightSqlServiceImpl > mockStatic =
541- mockStatic (ArrowFlightSqlServiceImpl .class , CALLS_REAL_METHODS )
542- ) {
543- // mock createSingleSchemaRoot
544- VectorSchemaRoot mockRoot = mock (VectorSchemaRoot .class );
545- Schema mockSchema = mock (Schema .class );
546- when (mockRoot .getSchema ()).thenReturn (mockSchema );
547- mockArrowUtil .when (() -> ArrowUtil .createSingleSchemaRoot (anyString (), anyString ())).thenReturn (mockRoot );
548-
549- // mock serializeMetadata
550- mockStatic .when (() -> ArrowFlightSqlServiceImpl .serializeMetadata (mockSchema ))
551- .thenReturn (ByteBuffer .wrap ("mock" .getBytes (StandardCharsets .UTF_8 )));
552-
553- // call method
554- service .createPreparedStatement (request , callContext , listener );
555-
556- // wait executor
557- Thread .sleep (500 );
570+ String preparedStatementHandle = preparedStatementResult .getPreparedStatementHandle ().toStringUtf8 ();
571+ assertNotNull (preparedStatementHandle );
572+ assertEquals (query , realContext .getPreparedStatement (preparedStatementHandle ));
573+
574+ Schema datasetSchema = deserializeSchema (preparedStatementResult .getDatasetSchema ());
575+ assertEquals (2 , datasetSchema .getFields ().size ());
576+ assertEquals ("c1" , datasetSchema .getFields ().get (0 ).getName ());
577+ assertTrue (datasetSchema .getFields ().get (0 ).getType () instanceof org .apache .arrow .vector .types .pojo .ArrowType .Int );
578+ assertEquals ("c2" , datasetSchema .getFields ().get (1 ).getName ());
579+ assertTrue (datasetSchema .getFields ().get (1 ).getType () instanceof org .apache .arrow .vector .types .pojo .ArrowType .Utf8 );
580+
581+ Schema parameterSchema = deserializeSchema (preparedStatementResult .getParameterSchema ());
582+ assertTrue (parameterSchema .getFields ().isEmpty ());
583+ }
584+
585+ @ Test
586+ public void testCreatePreparedStatementFallsBackToPlaceholderSchemaForNonQueryStatement () throws Exception {
587+ String query = "SHOW DATABASES" ;
588+ FlightSql .ActionCreatePreparedStatementRequest request = mock (FlightSql .ActionCreatePreparedStatementRequest .class );
589+ when (request .getQuery ()).thenReturn (query );
590+ ArrowFlightSqlConnectContext realContext = new ArrowFlightSqlConnectContext ("token123" );
591+ when (sessionManager .validateAndGetConnectContext ("token123" )).thenReturn (realContext );
592+
593+ CapturingResultListener listener = new CapturingResultListener ();
594+ service .createPreparedStatement (request , mockCallContext , listener );
595+
596+ FlightSql .ActionCreatePreparedStatementResult preparedStatementResult = awaitPreparedStatementResult (listener );
597+
598+ String preparedStatementHandle = preparedStatementResult .getPreparedStatementHandle ().toStringUtf8 ();
599+ assertNotNull (preparedStatementHandle );
600+ assertEquals (query , realContext .getPreparedStatement (preparedStatementHandle ));
601+
602+ Schema datasetSchema = deserializeSchema (preparedStatementResult .getDatasetSchema ());
603+ assertEquals (1 , datasetSchema .getFields ().size ());
604+ assertEquals ("result" , datasetSchema .getFields ().get (0 ).getName ());
605+ assertTrue (datasetSchema .getFields ().get (0 ).getType () instanceof org .apache .arrow .vector .types .pojo .ArrowType .Int );
606+ assertTrue (datasetSchema .getFields ().get (0 ).isNullable ());
607+
608+ Schema parameterSchema = deserializeSchema (preparedStatementResult .getParameterSchema ());
609+ assertTrue (parameterSchema .getFields ().isEmpty ());
610+ }
611+
612+ @ Test
613+ public void testBuildSchemaFromQueryRestoresPreviousConnectContext () throws Exception {
614+ ConnectContext previous = new ConnectContext ();
615+ previous .setThreadLocalInfo ();
616+ ArrowFlightSqlConnectContext arrowContext = new ArrowFlightSqlConnectContext ("token123" );
617+ Method method = ArrowFlightSqlServiceImpl .class
618+ .getDeclaredMethod ("buildSchemaFromQuery" , ArrowFlightSqlConnectContext .class , String .class );
619+ method .setAccessible (true );
620+
621+ try {
622+ Schema schema = (Schema ) method .invoke (service , arrowContext , "SELECT 1 AS c1" );
623+ assertNotNull (schema );
624+ assertSame (previous , ConnectContext .get ());
625+ } finally {
626+ ConnectContext .remove ();
558627 }
559628 }
560629
@@ -611,6 +680,20 @@ private void setFinalField(Object target, String fieldName, Object value) throws
611680 field .set (target , value );
612681 }
613682
683+ private FlightSql .ActionCreatePreparedStatementResult awaitPreparedStatementResult (CapturingResultListener listener )
684+ throws Exception {
685+ assertTrue (listener .await (), "Timed out waiting for createPreparedStatement result" );
686+ assertNull (listener .error , "Unexpected createPreparedStatement error: " + listener .error );
687+ assertTrue (listener .completed );
688+ assertNotNull (listener .result );
689+ return Any .parseFrom (listener .result .getBody ()).unpack (FlightSql .ActionCreatePreparedStatementResult .class );
690+ }
691+
692+ private Schema deserializeSchema (ByteString schemaBytes ) throws IOException {
693+ return MessageSerializer .deserializeSchema (
694+ new ReadChannel (Channels .newChannel (new ByteArrayInputStream (schemaBytes .toByteArray ()))));
695+ }
696+
614697 @ Test
615698 public void testCacheClose () {
616699 ArrowFlightSqlServiceImpl service = new ArrowFlightSqlServiceImpl (mock (ArrowFlightSqlSessionManager .class ), null );
0 commit comments