Copilot commented on code in PR #18544:
URL: https://github.com/apache/pinot/pull/18544#discussion_r3303786589


##########
pinot-sql-ddl/src/main/java/org/apache/pinot/sql/ddl/reverse/CanonicalDdlEmitter.java:
##########
@@ -59,30 +78,131 @@ public static String emit(Schema schema, TableConfig 
config) {
   /// Renders the canonical DDL, scoped under `databaseName` when non-null. 
The database name
   /// is rendered as a leading `db.` qualifier on the table name; this matches 
the parser's
   /// `[db.]name` grammar.
+  ///
+  /// Dispatches to [#emitMaterializedView] when `config.isMaterializedView()` 
is true; otherwise
+  /// renders a regular `CREATE TABLE`.
   public static String emit(Schema schema, TableConfig config, @Nullable 
String databaseName) {
+    if (MaterializedViewPropertyExtractor.isMaterializedView(config)) {
+      return emitMaterializedView(schema, config, databaseName);
+    }
+    return emitTable(schema, config, databaseName);
+  }
+
+  private static String emitTable(Schema schema, TableConfig config, @Nullable 
String databaseName) {
     rejectUnsupportedSchemaMetadata(schema);
     StringBuilder sb = new StringBuilder(512);
 
-    String rawTableName = 
TableNameBuilder.extractRawTableName(config.getTableName());
-
-    // Derive the effective database: explicit argument wins; fall back to any 
db. prefix already
-    // embedded in the raw table name (e.g. analytics.events_OFFLINE → 
"analytics"). This ensures
-    // the no-database overload emit(schema, config) preserves a db-qualified 
table name rather
-    // than silently stripping it to a bare name that would land in the wrong 
database on replay.
-    String effectiveDb = databaseName;
-    if ((effectiveDb == null || effectiveDb.isEmpty()) && 
rawTableName.contains(".")) {
-      int dot = rawTableName.indexOf('.');
-      effectiveDb = rawTableName.substring(0, dot);
-    }
-    String displayName = rawTableName.contains(".") ? 
rawTableName.substring(rawTableName.indexOf('.') + 1)
-        : rawTableName;
+    QualifiedName name = resolveQualifiedName(config, databaseName);
 
     sb.append("CREATE TABLE ");
-    if (effectiveDb != null && !effectiveDb.isEmpty()) {
-      sb.append(SqlIdentifiers.quote(effectiveDb)).append('.');
+    appendQualifiedName(sb, name);
+    sb.append(" (\n");
+    appendColumnBlock(sb, schema);
+    sb.append(")\n");
+    appendPrimaryKey(sb, schema);
+
+    sb.append("TABLE_TYPE = 
").append(emitTableType(config.getTableType())).append('\n');
+
+    Map<String, String> props = PropertyExtractor.extract(config);
+    appendPropertiesAndTerminate(sb, props);
+    return sb.toString();
+  }
+
+  /// Renders `CREATE MATERIALIZED VIEW [db.]name ( ... ) [REFRESH EVERY 
'<period>']
+  /// [PROPERTIES (...)] AS <definedSQL>;`.
+  ///
+  /// Failure modes:
+  /// - `task.MaterializedViewTask.definedSQL` missing/empty (defensive — 
caller should have
+  ///   gated on [#emit]'s [TableConfig#isMaterializedView] check, and the 
SPI's
+  ///   `TableConfigUtils.validateMaterializedViewInvariants` makes this state 
unreachable for
+  ///   any persisted config) → IllegalArgumentException so the controller 
surfaces a 400 rather
+  ///   than emit headerless / bodiless DDL.
+  /// - `task.MaterializedViewTask.schedule` present but 
[MaterializedViewPropertyRouter#cronToPeriod]
+  ///   returns null (non-standard / hand-typed cron) → 
IllegalArgumentException (Q1=A
+  ///   contract). The DDL grammar has no syntax for raw cron, so silently 
dropping the
+  ///   schedule on `SHOW CREATE MATERIALIZED VIEW` would let `emit → parse → 
emit` produce a
+  ///   config that runs on the cluster-wide cron instead of the 
operator-chosen one — a
+  ///   user-invisible behavioural change. The controller maps the exception 
to 400 with the
+  ///   message routed back to the caller.
+  private static String emitMaterializedView(Schema schema, TableConfig config,
+      @Nullable String databaseName) {
+    rejectUnsupportedSchemaMetadata(schema);
+    StringBuilder sb = new StringBuilder(512);
+
+    QualifiedName name = resolveQualifiedName(config, databaseName);
+    Map<String, String> mvTaskConfig = config.getTaskConfig()
+        .getConfigsForTaskType(MaterializedViewTask.TASK_TYPE);
+    String definedSql = mvTaskConfig.get(MaterializedViewTask.DEFINED_SQL_KEY);

Review Comment:
   `emitMaterializedView()` dereferences `config.getTaskConfig()` without a 
null check. If a TableConfig has `isMaterializedView=true` but its task config 
is missing/corrupted (or not yet validated), this will NPE before the intended 
`definedSQL` guard and will likely surface as a 500 instead of an actionable 
400. Consider defensively checking `config.getTaskConfig()` (and the returned 
`mvTaskConfig` map) for null and throwing an `IllegalArgumentException` with a 
clear message when missing.



##########
pinot-sql-ddl/src/main/java/org/apache/pinot/sql/ddl/reverse/SqlIdentifiers.java:
##########
@@ -36,6 +36,8 @@ final class SqlIdentifiers {
       // grammar keywords introduced by Pinot DDL that could appear as user 
identifiers
       "DIMENSION", "METRIC", "DATETIME", "FORMAT", "GRANULARITY", "OFFLINE", 
"REALTIME",
       "PROPERTIES", "TABLES", "TABLE_TYPE", "IF", "TYPE", "FROM",
+      // additional keywords introduced by the CREATE / SHOW CREATE 
MATERIALIZED VIEW grammar
+      "MATERIALIZED", "VIEW", "REFRESH", "EVERY",
       // standard SQL keywords that round-trip incorrectly when bare

Review Comment:
   The quoting allow-list adds MV keywords but omits the plural token `VIEWS` 
(used by `SHOW MATERIALIZED VIEWS`). For consistency with other Pinot-DDL 
keywords in `ALWAYS_QUOTE`, and to avoid emitting ambiguous/unquoted 
identifiers named `views`, add `"VIEWS"` to `ALWAYS_QUOTE`.



##########
pinot-materialized-view/src/test/java/org/apache/pinot/materializedview/metadata/MaterializedViewMetadataTest.java:
##########
@@ -154,4 +155,89 @@ public void testWatermarkAlwaysWritten() {
     assertTrue(znRecord.getSimpleFields().containsKey("watermarkMs"),
         "watermarkMs key must always be written");
   }
+
+  /// Definition equality is the controller's idempotency check at CREATE MV 
time: a second
+  /// CREATE with the same definedSQL must hash/compare equal to the first so 
a stale orphan
+  /// znode from a prior failed CREATE doesn't get classified as "different 
content".
+  /// Verify every persisted field round-trips through equals/hashCode.
+  @Test

Review Comment:
   The comment says definition equality/hashCode covers “every persisted 
field”, but the very next test notes `MaterializedViewSplitSpec` intentionally 
excludes `materializedViewTimeFormat` from equality even though it is persisted 
in the ZNRecord. Please tighten the wording here to avoid implying a stronger 
contract than the implementation provides.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to