|
1 | 1 | package com.linkedin.hoptimator.jdbc; |
2 | 2 |
|
3 | 3 | import com.linkedin.hoptimator.Deployer; |
| 4 | +import com.linkedin.hoptimator.Job; |
| 5 | +import com.linkedin.hoptimator.Pipeline; |
| 6 | +import com.linkedin.hoptimator.Sink; |
4 | 7 | import com.linkedin.hoptimator.jdbc.ddl.SqlCreateMaterializedView; |
| 8 | +import com.linkedin.hoptimator.util.DeploymentService; |
5 | 9 | import com.linkedin.hoptimator.jdbc.ddl.SqlCreateTable; |
6 | 10 | import com.linkedin.hoptimator.util.planner.PipelineRel; |
7 | 11 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; |
|
26 | 30 | import org.apache.calcite.util.Pair; |
27 | 31 | import org.junit.jupiter.api.Test; |
28 | 32 | import org.junit.jupiter.api.extension.ExtendWith; |
| 33 | +import org.mockito.Mock; |
| 34 | +import org.mockito.MockedStatic; |
29 | 35 | import org.mockito.junit.jupiter.MockitoExtension; |
30 | 36 |
|
31 | 37 | import java.sql.SQLException; |
|
41 | 47 | import static org.junit.jupiter.api.Assertions.assertNull; |
42 | 48 | import static org.junit.jupiter.api.Assertions.assertThrows; |
43 | 49 | import static org.junit.jupiter.api.Assertions.assertTrue; |
| 50 | +import static org.mockito.ArgumentMatchers.any; |
44 | 51 | import static org.mockito.Mockito.mock; |
45 | 52 | import static org.mockito.Mockito.when; |
46 | 53 |
|
|
50 | 57 | justification = "Mock objects created in stubbing setup don't need resource management") |
51 | 58 | class HoptimatorDdlUtilsTest { |
52 | 59 |
|
| 60 | + @Mock |
| 61 | + MockedStatic<DeploymentService> mockDeploymentService; |
| 62 | + |
53 | 63 | @Test |
54 | 64 | void testRenameColumnsReturnsQueryWhenColumnListNull() { |
55 | 65 | SqlNode query = SqlLiteral.createCharString("test", SqlParserPos.ZERO); |
@@ -528,6 +538,93 @@ void specifyFromSqlWithCreateMaterializedViewDryRunDoesNotThrowOnSchemaFailure() |
528 | 538 | } |
529 | 539 | } |
530 | 540 |
|
| 541 | + // ── specifyFromSql() plain SELECT path ─────────────────────────────────────── |
| 542 | + |
| 543 | + @Test |
| 544 | + void specifyFromSqlForPlainSelectReturnsSinkPathAndRowType() throws Exception { |
| 545 | + // Regression test: before the fix, pipeline.job().sink() was null for plain SELECT |
| 546 | + // (no INSERT INTO target), causing NullPointerException. Now setSink() anchors a virtual |
| 547 | + // "DEFAULT"."sink" so the pipeline can be fully constructed. |
| 548 | + Pipeline mockPipeline = mock(Pipeline.class); |
| 549 | + Sink mockSink = mock(Sink.class); |
| 550 | + Job mockJob = mock(Job.class); |
| 551 | + when(mockPipeline.sources()).thenReturn(Collections.emptyList()); |
| 552 | + when(mockPipeline.sink()).thenReturn(mockSink); |
| 553 | + when(mockPipeline.job()).thenReturn(mockJob); |
| 554 | + |
| 555 | + PipelineRel.Implementor mockPlan = mock(PipelineRel.Implementor.class); |
| 556 | + when(mockPlan.pipeline(any(), any())).thenReturn(mockPipeline); |
| 557 | + |
| 558 | + mockDeploymentService.when(() -> DeploymentService.plan(any(), any(), any())).thenReturn(mockPlan); |
| 559 | + mockDeploymentService.when(() -> DeploymentService.specify(any(), any())) |
| 560 | + .thenReturn(Collections.emptyList()); |
| 561 | + |
| 562 | + HoptimatorDriver driver = new HoptimatorDriver(); |
| 563 | + try (HoptimatorConnection conn = |
| 564 | + (HoptimatorConnection) driver.connect("jdbc:hoptimator://catalogs=util", new Properties())) { |
| 565 | + HoptimatorDdlUtils.SpecifyResult result = HoptimatorDdlUtils.specifyFromSql( |
| 566 | + "SELECT 1 AS \"KEY\", 'value' AS \"VAL\"", conn); |
| 567 | + |
| 568 | + // Specs: empty (no deployers return anything) |
| 569 | + assertNotNull(result.specs); |
| 570 | + assertTrue(result.specs.isEmpty()); |
| 571 | + // viewPath last element is "SINK" (uppercase) for a bare SELECT, matching original plan() behavior |
| 572 | + assertNotNull(result.viewPath); |
| 573 | + assertEquals("SINK", result.viewPath.get(result.viewPath.size() - 1)); |
| 574 | + // sinkRowType matches the SELECT output columns |
| 575 | + assertNotNull(result.sinkRowType); |
| 576 | + assertEquals(2, result.sinkRowType.getFieldCount()); |
| 577 | + assertEquals("KEY", result.sinkRowType.getFieldList().get(0).getName()); |
| 578 | + assertEquals("VAL", result.sinkRowType.getFieldList().get(1).getName()); |
| 579 | + } |
| 580 | + } |
| 581 | + |
| 582 | + @Test |
| 583 | + void specifyFromSqlCreateTableViewPathMatchesSchemaAndName() throws Exception { |
| 584 | + // For CREATE TABLE, the SpecifyResult.viewPath should include the schema path + table name. |
| 585 | + HoptimatorDriver driver = new HoptimatorDriver(); |
| 586 | + try (HoptimatorConnection conn = |
| 587 | + (HoptimatorConnection) driver.connect("jdbc:hoptimator://catalogs=util", new Properties())) { |
| 588 | + conn.calciteConnection().getRootSchema().add("VP_DB", new TestDatabaseSchema("vp-db")); |
| 589 | + HoptimatorDdlUtils.SpecifyResult result = HoptimatorDdlUtils.specifyFromSql( |
| 590 | + "CREATE TABLE \"VP_DB\".\"events\" (\"id\" INTEGER, \"msg\" VARCHAR)", conn); |
| 591 | + assertNotNull(result.viewPath); |
| 592 | + assertFalse(result.viewPath.isEmpty()); |
| 593 | + assertEquals("events", result.viewPath.get(result.viewPath.size() - 1)); |
| 594 | + assertTrue(result.viewPath.contains("VP_DB"), |
| 595 | + "viewPath should contain the schema: " + result.viewPath); |
| 596 | + } |
| 597 | + } |
| 598 | + |
| 599 | + @Test |
| 600 | + void specifyFromSqlForPlainSelectRestoresSchemaAfterCall() throws Exception { |
| 601 | + // The virtual "SINK" table registered during the SELECT path must not persist after |
| 602 | + // specifyFromSql returns (even when it throws at the deployer level). |
| 603 | + HoptimatorDriver driver = new HoptimatorDriver(); |
| 604 | + try (HoptimatorConnection conn = |
| 605 | + (HoptimatorConnection) driver.connect("jdbc:hoptimator://catalogs=util", new Properties())) { |
| 606 | + CalcitePrepare.Context ctx = conn.createPrepareContext(); |
| 607 | + |
| 608 | + // Capture schema state before the call. |
| 609 | + Pair<CalciteSchema, String> pair = HoptimatorDdlUtils.schema(ctx, false, |
| 610 | + new SqlIdentifier("sink", SqlParserPos.ZERO)); |
| 611 | + boolean existedBefore = pair.left != null |
| 612 | + && pair.left.plus().tables().get(pair.right) != null; |
| 613 | + |
| 614 | + try { |
| 615 | + HoptimatorDdlUtils.specifyFromSql("SELECT 1 AS \"COL1\"", conn); |
| 616 | + } catch (Exception ignored) { |
| 617 | + // Expected: planning/deployer failure in the test environment. |
| 618 | + } |
| 619 | + |
| 620 | + // Schema must be in the same state as before. |
| 621 | + boolean existsAfter = pair.left != null |
| 622 | + && pair.left.plus().tables().get(pair.right) != null; |
| 623 | + assertEquals(existedBefore, existsAfter, |
| 624 | + "Virtual SINK table must be removed after specifyFromSql returns"); |
| 625 | + } |
| 626 | + } |
| 627 | + |
531 | 628 | @Test |
532 | 629 | void optionsParsesSingleKeyValue() { |
533 | 630 | SqlNodeList list = new SqlNodeList(SqlParserPos.ZERO); |
|
0 commit comments