Repository navigation
[SPARK-60067[SQL] Keep UpdateFields compact through execution #59283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
6ccb18b
3790650
ca280d6
ce59df6
edc65d8
9980682
049f52e
013eaf9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,7 @@ import org.apache.spark.sql.catalyst.expressions.Cast._ | |
| import org.apache.spark.sql.catalyst.expressions.codegen._ | ||
| import org.apache.spark.sql.catalyst.expressions.codegen.Block._ | ||
| import org.apache.spark.sql.catalyst.parser.CatalystSqlParser | ||
| import org.apache.spark.sql.catalyst.trees.{LeafLike, UnaryLike} | ||
| import org.apache.spark.sql.catalyst.trees.{LeafLike, TreeNodeTag, UnaryLike} | ||
| import org.apache.spark.sql.catalyst.trees.TreePattern._ | ||
| import org.apache.spark.sql.catalyst.util._ | ||
| import org.apache.spark.sql.errors.QueryCompilationErrors | ||
|
|
@@ -764,12 +764,175 @@ case class DropField(name: String) extends StructFieldsOperation with LeafLike[E | |
| values.filterNot { case (field, _) => resolver(field.name, name) } | ||
| } | ||
|
|
||
| /** | ||
| * Builds a struct from fields copied from `structExpr` and values computed by `updatedValues`. | ||
| * | ||
| * `outputOrdinals` describes each output position. A nonnegative entry copies that field from | ||
| * `structExpr`; a negative entry evaluates the next expression in `updatedValues`. Storing copied | ||
| * fields as ordinals avoids adding a `GetStructField` child for every copied field. Row construction | ||
| * still visits every output field. | ||
| * | ||
| * For example, to transform `(a, b, c)` into `(a, newB, c, newD)`, `outputOrdinals` is | ||
| * `Seq(0, -1, 2, -1)` and `updatedValues` is `Seq(newB, newD)`. The first and third output fields | ||
| * copy source fields 0 and 2. The negative entries use `newB` and `newD`, in that order. | ||
| * | ||
| * If a nullable `structExpr` evaluates to null, the result is null and `updatedValues` are not | ||
| * evaluated. | ||
| */ | ||
| private case class UpdateFieldsExpression( | ||
| structExpr: Expression, | ||
| updatedValues: Seq[Expression], | ||
| outputOrdinals: Seq[Int], | ||
| outputFields: Seq[StructField]) extends Expression { | ||
|
|
||
| override def children: Seq[Expression] = structExpr +: updatedValues | ||
|
|
||
| override def nullable: Boolean = structExpr.nullable | ||
|
|
||
| override lazy val dataType: StructType = { | ||
| val sourceFields = structExpr.dataType.asInstanceOf[StructType] | ||
| var updatedIndex = 0 | ||
| StructType(outputOrdinals.zipWithIndex.map { case (sourceOrdinal, index) => | ||
| if (sourceOrdinal >= 0) { | ||
| sourceFields(sourceOrdinal) | ||
| } else { | ||
| val updatedValue = updatedValues(updatedIndex) | ||
| updatedIndex += 1 | ||
| outputFields(index).copy( | ||
| dataType = updatedValue.dataType, | ||
| nullable = updatedValue.nullable) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| override def eval(input: InternalRow): Any = { | ||
| val needsSource = nullable || outputOrdinals.exists(_ >= 0) | ||
| val sourceRow = if (needsSource) { | ||
| structExpr.eval(input).asInstanceOf[InternalRow] | ||
| } else { | ||
| null | ||
| } | ||
| if (sourceRow == null && nullable) { | ||
| null | ||
| } else { | ||
| val values = new Array[Any](outputOrdinals.length) | ||
| var updatedIndex = 0 | ||
| var i = 0 | ||
| while (i < outputOrdinals.length) { | ||
| val sourceOrdinal = outputOrdinals(i) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit (P3): Verification:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Converted per-row indexed state to arrays and cached needsSource. The evaluation loop no longer indexes Scala Lists or rescans source requirements.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, the per-row loop now uses the arrays and is linear. The same List indexing is still in |
||
| if (sourceOrdinal >= 0) { | ||
| values(i) = if (sourceRow == null) { | ||
| null | ||
| } else { | ||
| sourceRow.get(sourceOrdinal, dataType(i).dataType) | ||
| } | ||
| } else { | ||
| values(i) = updatedValues(updatedIndex).eval(input) | ||
| updatedIndex += 1 | ||
| } | ||
| i += 1 | ||
| } | ||
| new GenericInternalRow(values) | ||
| } | ||
| } | ||
|
|
||
| override protected def withNewChildrenInternal( | ||
| newChildren: IndexedSeq[Expression]): UpdateFieldsExpression = | ||
| copy(structExpr = newChildren.head, updatedValues = newChildren.tail) | ||
|
|
||
| override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = { | ||
| val rowClass = classOf[GenericInternalRow].getName | ||
| val values = ctx.freshName("values") | ||
| val needsSource = nullable || outputOrdinals.exists(_ >= 0) | ||
| val sourceCode = if (needsSource) Some(structExpr.genCode(ctx)) else None | ||
| val sourceRow = ctx.freshName("sourceRow") | ||
|
|
||
| val copyValuesCode = if (outputOrdinals.exists(_ >= 0)) { | ||
| val index = ctx.freshName("i") | ||
| val sourceOrdinal = ctx.freshName("sourceOrdinal") | ||
| val sourceOrdinals = ctx.addReferenceObj("sourceOrdinals", outputOrdinals.toArray, "int[]") | ||
| val fieldTypes = ctx.addReferenceObj( | ||
| "fieldTypes", | ||
| dataType.fields.map(_.dataType).toArray, | ||
| s"${classOf[DataType].getName}[]") | ||
| s""" | ||
| |for (int $index = 0; $index < ${outputOrdinals.length}; $index++) { | ||
| | int $sourceOrdinal = $sourceOrdinals[$index]; | ||
| | if ($sourceOrdinal >= 0) { | ||
| | $values[$index] = $sourceRow == null ? null : | ||
| | $sourceRow.get($sourceOrdinal, $fieldTypes[$index]); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking (P2): The previous lowering read each copied field through Verification:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generated copies now use CodeGenerator.getValue, grouped by field type. Added vectorized Parquet regressions for unchanged TIME, GEOMETRY, and GEOGRAPHY fields.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, verified. Copied fields now go through CodeGenerator.getValue per type group, and the new vectorized Parquet tests cover TIME, GEOMETRY, and GEOGRAPHY. |
||
| | } | ||
| |} | ||
| """.stripMargin | ||
| } else { | ||
| "" | ||
| } | ||
|
|
||
| val updatedOutputOrdinals = outputOrdinals.indices.filter(outputOrdinals(_) < 0) | ||
| assert(updatedOutputOrdinals.length == updatedValues.length) | ||
| val updatedCodes = updatedValues.zip(updatedOutputOrdinals).map { case (expr, ordinal) => | ||
| val eval = expr.genCode(ctx) | ||
| s""" | ||
| |${eval.code} | ||
| |if (${eval.isNull}) { | ||
| | $values[$ordinal] = null; | ||
| |} else { | ||
| | $values[$ordinal] = ${eval.value}; | ||
| |} | ||
| """.stripMargin | ||
| } | ||
| val updatedValuesCode = if (updatedCodes.nonEmpty) { | ||
| ctx.splitExpressionsWithCurrentInputs( | ||
| expressions = updatedCodes, | ||
| funcName = "updateFields", | ||
| extraArguments = "Object[]" -> values :: Nil) | ||
| } else { | ||
| "" | ||
| } | ||
|
|
||
| val outputCode = s""" | ||
| |Object[] $values = new Object[${outputOrdinals.length}]; | ||
| |${copyValuesCode} | ||
| |${updatedValuesCode} | ||
| |${ev.value} = new $rowClass($values); | ||
| |$values = null; | ||
| """.stripMargin | ||
|
|
||
| sourceCode match { | ||
| case Some(source) if nullable => | ||
| ev.copy(code = code""" | ||
| |${source.code} | ||
| |boolean ${ev.isNull} = ${source.isNull}; | ||
| |InternalRow ${ev.value} = null; | ||
| |if (!${ev.isNull}) { | ||
| | InternalRow $sourceRow = ${source.value}; | ||
| | $outputCode | ||
| |} | ||
| """.stripMargin) | ||
| case Some(source) => | ||
| ev.copy(code = code""" | ||
| |${source.code} | ||
| |InternalRow $sourceRow = ${source.value}; | ||
| |${CodeGenerator.javaType(dataType)} ${ev.value}; | ||
| |$outputCode | ||
| """.stripMargin, isNull = FalseLiteral) | ||
| case None => | ||
| ev.copy(code = code""" | ||
| |${CodeGenerator.javaType(dataType)} ${ev.value}; | ||
| |$outputCode | ||
| """.stripMargin, isNull = FalseLiteral) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Updates fields in a struct. | ||
| */ | ||
| case class UpdateFields(structExpr: Expression, fieldOps: Seq[StructFieldsOperation]) | ||
| extends Unevaluable { | ||
|
|
||
| private lazy val generatedStructReadOwner = UpdateFields.generatedStructReadOwner(this) | ||
|
|
||
| final override val nodePatterns: Seq[TreePattern] = Seq(UPDATE_FIELDS) | ||
|
|
||
| override def checkInputDataTypes(): TypeCheckResult = { | ||
|
|
@@ -783,7 +946,7 @@ case class UpdateFields(structExpr: Expression, fieldOps: Seq[StructFieldsOperat | |
| "inputSql" -> toSQLExpr(structExpr), | ||
| "inputType" -> toSQLType(structExpr.dataType)) | ||
| ) | ||
| } else if (newExprs.isEmpty) { | ||
| } else if (newFieldSources.isEmpty) { | ||
| DataTypeMismatch( | ||
| errorSubClass = "CANNOT_DROP_ALL_FIELDS", | ||
| messageParameters = Map.empty | ||
|
|
@@ -806,35 +969,129 @@ case class UpdateFields(structExpr: Expression, fieldOps: Seq[StructFieldsOperat | |
|
|
||
| override def prettyName: String = "update_fields" | ||
|
|
||
| private lazy val newFieldExprs: Seq[(StructField, Expression)] = { | ||
| def getFieldExpr(i: Int): Expression = structExpr match { | ||
| case c: CreateNamedStruct => c.valExprs(i) | ||
| case _ => GetStructField(structExpr, i) | ||
| } | ||
| val fieldsWithIndex = structExpr.dataType.asInstanceOf[StructType].fields.zipWithIndex | ||
| val existingFieldExprs: Seq[(StructField, Expression)] = | ||
| fieldsWithIndex.map { case (field, i) => (field, getFieldExpr(i)) }.toImmutableArraySeq | ||
| fieldOps.foldLeft(existingFieldExprs)((exprs, op) => op(exprs)) | ||
| } | ||
| private lazy val newFieldSources: Seq[(StructField, UpdateFields.FieldSource)] = | ||
| UpdateFields.fieldSources(structExpr.dataType.asInstanceOf[StructType], fieldOps) | ||
|
|
||
| private lazy val newFields: Seq[StructField] = newFieldExprs.map(_._1) | ||
| private lazy val newFields: Seq[StructField] = newFieldSources.map(_._1) | ||
|
|
||
| lazy val newExprs: Seq[Expression] = newFieldExprs.map(_._2) | ||
| /** Returns the expression for one output field without constructing the complete struct. */ | ||
| def newExpr(ordinal: Int): Expression = newFieldSources(ordinal)._2 match { | ||
| case UpdateFields.Original(sourceOrdinal) => getFieldExpr(sourceOrdinal) | ||
| case UpdateFields.Updated(expr) => expr | ||
| } | ||
|
|
||
| lazy val evalExpr: Expression = { | ||
| val createNamedStructExpr = CreateNamedStruct(newFieldExprs.flatMap { | ||
| case (field, expr) => Seq(Literal(field.name), expr) | ||
| }) | ||
| /** | ||
| * Returns an input field expression and marks generated reads so they can share `structExpr` | ||
| * without changing equivalent expressions supplied to `withField`. | ||
| */ | ||
| private def getFieldExpr(ordinal: Int): Expression = structExpr match { | ||
| case c: CreateNamedStruct => c.valExprs(ordinal) | ||
| case _ => UpdateFields.markGeneratedStructRead( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking (P2): See Shared repair plan 1 in the review body.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the generated-read tags and simplification guards. Ordinary field extraction simplifies again, with a regression covering an unused throwing update.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, the tag-based block is gone and ordinary extraction simplifies again. One related regression: the revision also removed the merge-target |
||
| GetStructField(structExpr, ordinal), generatedStructReadOwner) | ||
| } | ||
|
|
||
| if (structExpr.nullable) { | ||
| If(IsNull(structExpr), Literal(null, dataType), createNamedStructExpr) | ||
| } else { | ||
| createNamedStructExpr | ||
| lazy val evalExpr: Expression = With(structExpr) { case Seq(structRef) => | ||
| // Read fields used to rebuild the input struct from structRef so structExpr is evaluated once. | ||
| // Do not rewrite expressions passed to withField; they must be evaluated independently. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit (P3): This reads as the opposite of what the next lines do. Verification:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the comment along with the identity/tag-based rewriting machinery. Nested updates are now represented explicitly.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, verified. The comment went away with the rewriting machinery it described. |
||
| def replaceStruct(expr: Expression): Expression = expr.transformDown { | ||
| case field @ GetStructField(child, _, _) | ||
| if UpdateFields.isGeneratedStructRead(field, generatedStructReadOwner) && | ||
| child.eq(structExpr) => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking (P1): This only shares the input when the generated read's child is the same object as See Shared repair plan 1 in the review body.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replaced identity-based sharing with UpdateNestedField, which applies nested operations to the current field value without duplicating the parent input. Added unresolved-column and input-dependent nondeterministic regressions.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, verified on the current head. UpdateNestedField applies nested operations to the current field value, so the parent input is no longer duplicated, and the With in evalExpr evaluates a non-literal input once. The new unresolved-column and nondeterministic-input tests cover the path I raised. |
||
| field.copy(child = structRef) | ||
| } | ||
|
|
||
| val updatedValues = ArrayBuffer.empty[Expression] | ||
| val outputOrdinals = newFieldSources.map { case (_, source) => | ||
| source match { | ||
| case UpdateFields.Original(ordinal) => structExpr match { | ||
| case namedStruct: CreateNamedStruct => | ||
| updatedValues += namedStruct.valExprs(ordinal) | ||
| -1 | ||
| case _ => ordinal | ||
| } | ||
| case UpdateFields.Updated(expr) => | ||
| updatedValues += replaceStruct(expr) | ||
| -1 | ||
| } | ||
| } | ||
| UpdateFieldsExpression(structRef, updatedValues.toSeq, outputOrdinals, newFields) | ||
| } | ||
| } | ||
|
|
||
| object UpdateFields { | ||
| /** Describes where an output field gets its value without creating an expression for a copy. */ | ||
| private sealed trait FieldSource | ||
|
|
||
| /** Copies the field at `ordinal` from the input struct. */ | ||
| private case class Original(ordinal: Int) extends FieldSource | ||
|
|
||
| /** Computes the field from an expression supplied to `withField`. */ | ||
| private case class Updated(expr: Expression) extends FieldSource | ||
|
|
||
| /** | ||
| * Applies the requested field operations while retaining whether each output field is copied or | ||
| * computed. This provenance lets evaluation copy unchanged fields by ordinal and lets extraction | ||
| * return one field without constructing the complete struct. | ||
| */ | ||
| private def fieldSources( | ||
| structType: StructType, | ||
| fieldOps: Seq[StructFieldsOperation]): Seq[(StructField, FieldSource)] = { | ||
| val fields: Seq[(StructField, FieldSource)] = structType.fields.zipWithIndex.map { | ||
| case (field, ordinal) => | ||
| field -> Original(ordinal) | ||
| }.toImmutableArraySeq | ||
| fieldOps.foldLeft(fields) { | ||
| case (current, op: WithField) => | ||
| val updated = StructField(op.name, op.valExpr.dataType, op.valExpr.nullable) -> | ||
| Updated(op.valExpr) | ||
| val result = ArrayBuffer.empty[(StructField, FieldSource)] | ||
| var hasMatch = false | ||
| current.foreach { fieldSource => | ||
| if (op.resolver(fieldSource._1.name, op.name)) { | ||
| hasMatch = true | ||
| result += updated | ||
| } else { | ||
| result += fieldSource | ||
| } | ||
| } | ||
| if (!hasMatch) result += updated | ||
| result.toSeq | ||
| case (current, op: DropField) => | ||
| current.filterNot { case (field, _) => op.resolver(field.name, op.name) } | ||
| case (_, op) => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit (P3): Verification:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the unused StructFieldsOperation.apply implementations and updated the documentation. Name matching now lives in fieldSources.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, verified. The apply implementations and their scaladoc are gone, and name matching now lives only in fieldSources. |
||
| throw SparkException.internalError( | ||
| s"Unsupported StructFieldsOperation: ${op.getClass.getName}") | ||
| } | ||
| } | ||
|
|
||
| // Generated reads identify field accesses introduced while expanding nested paths. The owner | ||
| // prevents one UpdateFields from rewriting reads that belong to another UpdateFields. | ||
| private val GENERATED_STRUCT_READ = TreeNodeTag[ExprId]("generatedStructRead") | ||
| private val GENERATED_STRUCT_READ_OWNER = TreeNodeTag[ExprId]("generatedStructReadOwner") | ||
|
|
||
| /** Returns the stable identity used to associate generated reads with `expr`. */ | ||
| private def generatedStructReadOwner(expr: Expression): ExprId = { | ||
| expr.getTagValue(GENERATED_STRUCT_READ_OWNER).getOrElse { | ||
| val owner = NamedExpression.newExprId | ||
| expr.setTagValue(GENERATED_STRUCT_READ_OWNER, owner) | ||
| owner | ||
| } | ||
| } | ||
|
|
||
| /** Marks a field read as generated by the UpdateFields identified by `owner`. */ | ||
| private def markGeneratedStructRead[T <: Expression](expr: T, owner: ExprId): T = { | ||
| expr.setTagValue(GENERATED_STRUCT_READ, owner) | ||
| expr | ||
| } | ||
|
|
||
| /** Tests whether a field read was generated by the UpdateFields identified by `owner`. */ | ||
| private def isGeneratedStructRead(expr: Expression, owner: ExprId): Boolean = | ||
| expr.getTagValue(GENERATED_STRUCT_READ).contains(owner) | ||
|
|
||
| /** Tests whether a field read was generated while expanding any nested update path. */ | ||
| private[catalyst] def isGeneratedStructRead(expr: Expression): Boolean = | ||
| expr.containsTag(GENERATED_STRUCT_READ) | ||
|
|
||
| private def nameParts(fieldName: String): Seq[String] = { | ||
| require(fieldName != null, "fieldName cannot be null") | ||
|
|
||
|
|
@@ -878,7 +1135,9 @@ object UpdateFields { | |
| structExpr = newStruct, | ||
| namePartsRemaining = namePartsRemaining.tail, | ||
| valueFunc = valueFunc) | ||
| UpdateFields(structExpr, WithField(fieldName, newValue) :: Nil) | ||
| val updated = UpdateFields(structExpr, WithField(fieldName, newValue) :: Nil) | ||
| markGeneratedStructRead(newStruct, generatedStructReadOwner(updated)) | ||
| updated | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -301,7 +301,9 @@ abstract class Optimizer(catalogManager: CatalogManager) | |
| RemoveNoopOperators), | ||
| // This batch must be executed after the `RewriteSubquery` batch, which creates joins. | ||
| Batch("NormalizeFloatingNumbers", Once, NormalizeFloatingNumbers), | ||
| Batch("ReplaceUpdateFieldsExpression", Once, ReplaceUpdateFieldsExpression))) | ||
| // Repeat because an update can contain another update. Keep the resulting With expressions so | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit (P3): Moving off Verification:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Restored
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, verified. The batch is Once again and the comment is gone. The new suite checks that nested updates lower fully in one pass and that a second application changes nothing. |
||
| // their definitions remain inside conditional branches until evaluation. | ||
| Batch("ReplaceUpdateFieldsExpression", fixedPoint, ReplaceUpdateFieldsExpression))) | ||
|
|
||
| // remove any batches with no rules. this may happen when subclasses do not add optional rules. | ||
| batches.filter(_.rules.nonEmpty) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,7 +68,9 @@ object OptimizeUpdateFields extends Rule[LogicalPlan] { | |
| val newWithFields = newNames.map(n => WithField(nameMap(n), newValues(n))) | ||
| UpdateFields(structExpr, newWithFields.toSeq) | ||
|
|
||
| case UpdateFields(UpdateFields(struct, fieldOps1), fieldOps2) => | ||
| // Flattening would separate a nested field read from the struct it reads. | ||
| case UpdateFields(UpdateFields(struct, fieldOps1), fieldOps2) | ||
| if !(fieldOps1 ++ fieldOps2).exists(_.exists(UpdateFields.isGeneratedStructRead)) => | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking (P1): This guard also runs in the analyzer: See Shared repair plan 1 in the review body.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the guard and restored chain flattening. Nested operations compose before resolution. Added full-pipeline tests for chained nested withField and dropFields calls on unresolved columns.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, verified. The flatten case is back without the guard, nested operations merge in optimizeFieldOps, and the new test bounds analyzed and optimized plan size for chained nested updates on unresolved columns. |
||
| UpdateFields(struct, fieldOps1 ++ fieldOps2) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit (P3):
outputFields(index)andupdatedValues(updatedIndex)are positional lookups, and the sequencesevalExprpasses in areLists on Scala 2.13 (updatedValues.toSeqfrom anArrayBuffer, and thefieldSourcesresult after anyWithField). Each lookup is O(index), so building this type is O(F^2) for an F-field struct, and because it is a per-instance lazy val it is recomputed for every copy a tree transform makes.doGenCodehas the same pattern inoutputOrdinals.indices.filter(outputOrdinals(_) < 0)(line 891).The per-row
evalloop already usesindexedOrdinalsandindexedUpdatedValuesafter the earlier review. Using those arrays here too, or walking the sequences with iterators, would keep planning and codegen linear in width for the wide structs this PR targets.Verification:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 049f52e. Type construction uses indexed ordinals and iterators; codegen uses indexed ordinals. Added 100/500-field checks for output types, nullability, null inputs, and interpreted/codegen results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, verified. dataType now walks the indexed ordinals and updated values with iterators, and doGenCode uses indexedOrdinals. The 100- and 500-field tests cover types, nullability, and interpreted and codegen results.