Skip to content

feat: route translate and to_csv through codegen dispatch by default - #5032

Open
andygrove wants to merge 2 commits into
apache:mainfrom
andygrove:feat/incompat-codegen-dispatch
Open

feat: route translate and to_csv through codegen dispatch by default#5032
andygrove wants to merge 2 commits into
apache:mainfrom
andygrove:feat/incompat-codegen-dispatch

Conversation

@andygrove

@andygrove andygrove commented Jul 24, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

Closes #.

Rationale for this change

translate (StringTranslate) and to_csv (StructsToCsv) have native implementations that are incompatible with Spark by default, so today the whole enclosing operator falls back to Spark unless allowIncompatible is set. Routing them through the JVM codegen dispatcher instead keeps the operator on the native pipeline while producing bit-exact Spark results, and preserves the faster native path as an opt-in.

What changes are included in this PR?

  • Route translate (StringTranslate) and to_csv (StructsToCsv) through the JVM codegen dispatcher by default, using the same NativeOptInAvailable pattern as to_json and split. The dispatcher runs Spark's own generated code (bit-exact) while keeping the operator on the native pipeline; the native path stays available via allowIncompatible.
  • Update the docs to note the codegen-dispatch default for to_csv.

How are these changes tested?

  • Update the string_translate SQL test (previously asserting fallback) to verify native codegen dispatch.
  • Add a to_csv SQL file test.

@andygrove andygrove added this to the 1.0.0 milestone Jul 24, 2026
`translate` (StringTranslate) and `to_csv` (StructsToCsv) have native
implementations that are incompatible with Spark by default, so today the whole
enclosing operator falls back to Spark unless allowIncompatible is set. Route
them through the JVM codegen dispatcher by default instead, using the same
NativeOptInAvailable pattern as `to_json` and `split`: the dispatcher runs
Spark's own generated code (bit-exact) while keeping the operator on the native
pipeline, and the faster native path stays available via allowIncompatible.

Update the translate SQL test (previously asserting fallback) to verify native
dispatch, and add a to_csv test.
@andygrove
andygrove force-pushed the feat/incompat-codegen-dispatch branch from 7cd0e22 to 3abb614 Compare July 24, 2026 23:01
@andygrove andygrove changed the title feat: route pow, translate, and to_csv through codegen dispatch by default feat: route translate and to_csv through codegen dispatch by default Jul 24, 2026

@mbutrovich mbutrovich left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass, thanks @andygrove!

Comment on lines +134 to +136
val childExprs = expr.children.map(exprToProtoInternal(_, inputs, binding))
val optExpr = scalarFunctionExprToProto("translate", childExprs: _*)
optExprWithFallbackReason(optExpr, expr, expr.children: _*)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, lines 134-136 are a verbatim copy of CometScalarFunction.convert (spark/src/main/scala/org/apache/comet/serde/CometScalarFunction.scala:29-33), so the function name "translate" now lives in two places. Dropping the CometScalarFunction[StringTranslate]("translate") base to hand-inline its body is also a step away from the established idiom for this exact case, which keeps the base and overrides convert to choose a branch: CometInitCap (strings.scala:162-174), CometStringReplace (:192-199), CometCaseConversionBase (:67-78).

Second, and more to the point: the whole getSupportLevel plus convert branching structure is what CodegenDispatchFallback exists to express declaratively. QueryPlanSerde.exprToProtoInternal already routes a non-opted-in Incompatible result from such a serde through emitJvmCodegenDispatch and falls back to Spark only when the dispatcher refuses (QueryPlanSerde.scala:838-870, dispatchIfFallback at :1109-1117), and CodegenDispatchFallback extends NativeOptInAvailable so GenerateDocs renders the same compatible-by-default row (GenerateDocs.scala:193). So the entire diff to this object should be one mixin on the original:

object CometStringTranslate
    extends CometScalarFunction[StringTranslate]("translate")
    with CodegenDispatchFallback {
  private val incompatReason = ...
  override def getIncompatibleReasons(): Seq[String] = Seq(incompatReason)
  override def getSupportLevel(expr: StringTranslate): SupportLevel = Incompatible(Some(incompatReason))
}

That keeps the existing Incompatible declaration honest, keeps the function name in one place, and gets the logWarning on the opted-in path (QueryPlanSerde.scala:841-846) that the hand-rolled version drops.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right on both counts, and the mixin does the whole job. Done in 26994c9 — the entire change to this object is now with CodegenDispatchFallback on the original declaration, exactly as you wrote it. The CometScalarFunction[StringTranslate]("translate") base is restored, so "translate" lives in one place again, and the opted-in logWarning at QueryPlanSerde.scala:841-846 comes back with it. The pre-existing Incompatible(Some(incompatReason)) was already the right classification, so it stays untouched.

Comment on lines +262 to +303
object CometStructsToCsv extends CometCodegenDispatch[StructsToCsv] with NativeOptInAvailable {

private val incompatibleDataTypes = Seq(DateType, TimestampType, TimestampNTZType, BinaryType)

override def getIncompatibleReasons(): Seq[String] = Seq(
"Date, Timestamp, TimestampNTZ, and Binary data types may produce different results" +
" (https://github.com/apache/datafusion-comet/issues/3232)")

override def getUnsupportedReasons(): Seq[String] = Seq(
"Complex types (arrays, maps, structs) in the schema are not supported")

override def getSupportLevel(expr: StructsToCsv): SupportLevel = {
// The native ToCsv path only supports non-complex, compatible field types. Everything else
// (and the default, unless opted in) runs through the codegen dispatcher, which is bit-exact.
private def nativeSupported(expr: StructsToCsv): Boolean = {
val dataTypes = expr.inputSchema.fields.map(_.dataType)
val containsComplexType = dataTypes.exists(DataTypeSupport.isComplexType)
if (containsComplexType) {
return Unsupported(
Some(
s"The schema ${expr.inputSchema} is not supported because it includes a complex type"))
}
val containsIncompatibleDataTypes = dataTypes.exists(incompatibleDataTypes.contains)
if (containsIncompatibleDataTypes) {
return Incompatible(
Some(
s"The schema ${expr.inputSchema} is not supported because " +
s"it includes a incompatible data types: $incompatibleDataTypes"))
}
// https://github.com/apache/datafusion-comet/issues/3232
Incompatible()
!dataTypes.exists(DataTypeSupport.isComplexType) &&
!dataTypes.exists(incompatibleDataTypes.contains)
}

override def getSupportLevel(expr: StructsToCsv): SupportLevel =
if (!CometConf.isExprAllowIncompat(getExprConfigName(expr)) && nativeSupported(expr)) {
Compatible(nativeOptIn =
Some(NativeOptIn(CometConf.getExprAllowIncompatConfigKey(getExprConfigName(expr)))))
} else {
Compatible()
}

override def convert(
expr: StructsToCsv,
inputs: Seq[Attribute],
binding: Boolean): Option[ExprOuterClass.Expr] = {
for {
childProto <- exprToProtoInternal(expr.child, inputs, binding)
} yield {
val optionsProto = options2Proto(expr.options, expr.timeZoneId)
val toCsv = ExprOuterClass.ToCsv
.newBuilder()
.setChild(childProto)
.setOptions(optionsProto)
.build()
ExprOuterClass.Expr.newBuilder().setToCsv(toCsv).build()
if (CometConf.isExprAllowIncompat(getExprConfigName(expr)) && nativeSupported(expr)) {
for {
childProto <- exprToProtoInternal(expr.child, inputs, binding)
} yield {
val optionsProto = options2Proto(expr.options, expr.timeZoneId)
val toCsv = ExprOuterClass.ToCsv
.newBuilder()
.setChild(childProto)
.setOptions(optionsProto)
.build()
ExprOuterClass.Expr.newBuilder().setToCsv(toCsv).build()
}
} else {
// Default: route through the codegen dispatcher so Spark's own doGenCode runs inside the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same argument applies here, and more strongly because the pre-existing getSupportLevel already returned exactly the right classifications: Unsupported for complex field types, Incompatible for the #3232 types, Incompatible() otherwise. With with CodegenDispatchFallback on line 262 those three outcomes route as intended with no other change: Unsupported to the dispatcher plus a logDebug (QueryPlanSerde.scala:827-833), non-opted-in Incompatible to the dispatcher plus the [COMET-INFO] opt-in hint (:854-860), opted-in Incompatible to the native proto (:847). That turns a +30/-30 rewrite into a one-line change and removes the duplicated nativeSupported(expr) test that currently has to be kept in sync between :279 and :290.

Deleting getUnsupportedReasons (was structs.scala:270-271) also loses real information. The native path still cannot do complex types, and under NativeOptInAvailable the documented surface for native-path limitations is getIncompatibleReasons (spark/src/main/scala/org/apache/comet/serde/CometExpressionSerde.scala:52-63). Keeping CodegenDispatchFallback preserves both accessors as-is; if you keep the rewrite instead, the complex-type limitation needs folding into getIncompatibleReasons at :266-268, otherwise a user who sets allowIncompatible has no way to find out why they did not get the native path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — reverted to the original three-way getSupportLevel plus the mixin in 26994c9. The +30/-30 rewrite is now one mixin and a comment.

Confirming your reading of the routing, since it is the whole basis for the change: Unsupported (complex field types) reaches the dispatcher plus logDebug at QueryPlanSerde.scala:819-831, non-opted-in Incompatible (#3232 types, and the Incompatible() catch-all) reaches the dispatcher plus the [COMET-INFO] hint at :848-860, and opted-in Incompatible reaches handler.convert — the native proto — at :846. So all three original outcomes route as intended with no other change.

getUnsupportedReasons is restored, and the duplicated nativeSupported predicate is gone along with the sync hazard between the two call sites. I also dropped CometCodegenDispatch as the base: it hardcodes getSupportLevel = Compatible() and convert = emitJvmCodegenDispatch for expressions with no native path at all, so overriding convert to sometimes go native was working against it.

.setOptions(optionsProto)
.build()
ExprOuterClass.Expr.newBuilder().setToCsv(toCsv).build()
if (CometConf.isExprAllowIncompat(getExprConfigName(expr)) && nativeSupported(expr)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When isExprAllowIncompat is true and nativeSupported is false, the user asked for the native path and silently gets the dispatcher with nothing recorded. CometStructsToJson has the same hole so this is not new, but it deserves a logDebug naming the disqualifying field type. Using CodegenDispatchFallback gets this for free through the Unsupported arm.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by the redesign rather than by adding a logDebug: with CodegenDispatchFallback this case lands in the framework's Unsupported arm, which already logs at debug naming the disqualifying reason (QueryPlanSerde.scala:819-831). So it is covered for free, as you noted.

CREATE TABLE test_translate(s string, from_str string, to_str string) USING parquet

statement
INSERT INTO test_translate VALUES ('hello', 'el', 'ip'), ('hello', 'aeiou', '12345'), ('', 'a', 'b'), (NULL, 'a', 'b'), ('hello', '', ''), ('abc', 'abc', 'x')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The four query blocks do assert native execution, so flipping them off expect_fallback is real new coverage of the routing. What is missing is the payload: the header comment at :18-22 names two specific ways the native path diverges, and the six rows on line 28 are ASCII-only and hit neither. The file is now identical to string_translate_enabled.sql in CREATE TABLE, INSERT rows and queries, differing only in the config header, so nothing distinguishes the dispatcher from the native path here. Please add the two rows that are only assertable now that the default is bit-exact: a combining-mark input for the grapheme versus code-point difference, and a to argument containing U+0000 for the deletion-sentinel difference. To keep the fixture ASCII, build them with decode(X'65CC81', 'UTF-8') and decode(X'00', 'UTF-8') rather than embedding the characters. Those two rows are the regression test for this PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 26994c9, built with decode() as you suggested so the fixture stays ASCII:

(concat('caf', decode(X'65CC81', 'UTF-8')), 'e', 'E'),   -- "e" + U+0301: one grapheme, two code points
('hello', 'l', decode(X'00', 'UTF-8'))                 -- U+0000 as the `to` argument

I also checked that they actually discriminate rather than passing vacuously, since that is the point of them. Adding the same two rows to string_translate_enabled.sql, which opts into the native path, fails:

- sql-file: expressions/string/string_translate_enabled.sql *** FAILED ***
  Error executing SQL 'SELECT translate(s, from_str, to_str) FROM test_translate_enabled'
  Results do not match for query

So the rows pass under the dispatcher and fail under the native path, which makes them a real regression test for the routing. I left string_translate_enabled.sql as it was — its header explicitly says it covers inputs where the two implementations agree, so the divergent rows belong in the default file only.

Comment on lines 272 to 276
private def nativeSupported(expr: StructsToCsv): Boolean = {
val dataTypes = expr.inputSchema.fields.map(_.dataType)
val containsComplexType = dataTypes.exists(DataTypeSupport.isComplexType)
if (containsComplexType) {
return Unsupported(
Some(
s"The schema ${expr.inputSchema} is not supported because it includes a complex type"))
}
val containsIncompatibleDataTypes = dataTypes.exists(incompatibleDataTypes.contains)
if (containsIncompatibleDataTypes) {
return Incompatible(
Some(
s"The schema ${expr.inputSchema} is not supported because " +
s"it includes a incompatible data types: $incompatibleDataTypes"))
}
// https://github.com/apache/datafusion-comet/issues/3232
Incompatible()
!dataTypes.exists(DataTypeSupport.isComplexType) &&
!dataTypes.exists(incompatibleDataTypes.contains)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nativeSupported is the new branch predicate driving both getSupportLevel (:279) and convert (:290), and no test takes it false. The two cases it excludes are the reason the change exists:

Date, Timestamp, TimestampNTZ and Binary fields were Incompatible before this PR and fell the operator back to Spark. That is #3232.

Complex field types were Unsupported before and also fell back. They are legal to_csv input: StructsToCsv.checkInputDataTypes accepts arrays, maps and nested structs and rejects only Variant (Spark csvExpressions.scala:240-261 on master and branch-4.0; branch-3.5 has no gate at all). Nothing tests that the kernel can hand converter() a nested InternalRow, which is the new and untried part of this path.

The cheapest place to cover the first group is the existing suite rather than a new fixture. CometCsvExpressionSuite's test named "to_csv - default options" (spark/src/test/scala/org/apache/comet/CometCsvExpressionSuite.scala:36) is actually run under allowIncompatible=true (:50), and it selects c0-c5, c7, c8, c9 and c12 out of the 14-column fuzz schema, skipping exactly c6 Double, c10 Timestamp, c11 TimestampNTZ and c13 Binary (FuzzDataGenerator.scala:303-317). Those are the columns the native path cannot match Spark on. After this PR the dispatcher matches them by construction, so that test can drop the allowIncompatible wrapper and add those four columns, which is what would actually justify the name it already has.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took your suggestion for the first group and it works: in 26994c9 CometCsvExpressionSuite's "to_csv - default options" drops the allowIncompatible=true wrapper and now selects the whole fuzz schema rather than a hand-picked subset, so it covers c6 Double, c10 Timestamp and c11 TimestampNTZ. It finally tests what its name says.

c13 Binary I had to leave out, and not because of a divergence. Spark's CSV converter renders BinaryType with Java's default Object.toString(), so a row comes out as:

...,3128558622871195782,[B@731af74

That trailing token is an identity hash of the byte-array instance, so it differs between any two evaluations. With c13 included, every row mismatches and the only differing token is that hash — the double, timestamp and TimestampNTZ columns all match exactly:

![false,-1,-28684,...,3332-12-27T13:01:58.882-08:00,3332-12-15T18:12:14.824,...,[B@731af74]
 [false,-1,-28684,...,3332-12-27T13:01:58.882-08:00,3332-12-15T18:12:14.824,...,[B@72f0225a]

So the value is not assertable by any engine, including Spark against itself. I noted that in the test rather than silently dropping the column.

On the complex-type group, the news is worse and I could not write the test. You are right that checkInputDataTypes accepts arrays, maps and nested structs, so they reach the converter — but Spark's output for them is not a comparable value. Probed against Spark 3.5 with Comet disabled entirely (spark.comet.enabled=false), so this is pure Spark:

  • non-null values render as Java identity strings: org.apache.spark.sql.vectorized.ColumnarArray@1ada50f0, ColumnarRow@4dd6f621, ColumnarMap@71e38661
  • any null complex value throws NullPointerException inside UnsafeWriter.write(UnsafeWriter.java:110), with no Comet frames in the stack

So to_csv over complex types produces either an identity hash or a crash in Spark itself. There is no value-parity assertion to make, and I did not want to add a fixture that pins garbage. I documented the finding in to_csv.sql with the reasoning and the fact that it is Spark behavior.

Worth noting this is not a regression from the PR: before the change these schemas were Unsupported and fell the projection back to Spark, and now they reach the dispatcher, which runs the same Spark doGenCode. The NPE and the identity strings are identical either way — the change only decides whether the rest of the projection stays native. Happy to file a separate issue against Spark's to_csv for the complex-type behavior if you think it is worth tracking on our side.


-- column struct: values with delimiters and quotes exercise Spark's CSV quoting rules
query
SELECT to_csv(named_struct('a', a, 'b', b, 'c', c)) FROM test_to_csv

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options is the main axis of behavior difference for this expression and nothing in the new fixture varies it. CometCsvExpressionSuite:71-161 covers delimiter, escape, quoteAll and nullValue but only under allowIncompatible=true, so the dispatcher path has no options coverage anywhere. to_csv(s, map('sep', ';')) plus a timestampFormat over a timestamp field would cover both this and the #3232 types in one query.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 26994c9. to_csv.sql now covers map('sep', ';') over the existing rows (which also changes which values need quoting), plus a timestampFormat/dateFormat query over a new date+timestamp table — so the same query exercises both the options axis and the #3232 types that previously fell back. The dispatcher path had no options coverage anywhere before this.

@andygrove andygrove removed this from the 1.0.0 milestone Jul 27, 2026
Replace the hand-rolled getSupportLevel/convert branching with the
`CodegenDispatchFallback` mixin, which is what the framework already provides
for exactly this shape. `exprToProtoInternal` routes an `Unsupported` result to
the dispatcher plus a logDebug, a non-opted-in `Incompatible` to the dispatcher
plus the [COMET-INFO] opt-in hint, and an opted-in `Incompatible` to the native
proto with a logWarning.

For `CometStringTranslate` that makes the whole diff one mixin on the original
declaration: the pre-existing `Incompatible(Some(incompatReason))` was already
the right classification. This also restores the `CometScalarFunction` base, so
the function name "translate" stops being duplicated and the opted-in logWarning
comes back.

For `CometStructsToCsv` the original three-way `getSupportLevel` was likewise
already correct -- `Unsupported` for complex field types, `Incompatible` for the
apache#3232 types, `Incompatible()` otherwise -- so the +30/-30 rewrite collapses to
the mixin. That restores `getUnsupportedReasons`, drops the duplicated
`nativeSupported` predicate that had to stay in sync between two call sites, and
picks up the logDebug on the opted-in-but-unsupported path for free. Also drops
`CometCodegenDispatch` as the base, which is for expressions with no native
path at all.

Tests:

- string_translate.sql gains the two rows that actually distinguish the paths:
  "e" + U+0301 (one grapheme, two code points) and U+0000 as the `to` argument.
  Built with decode(X'65CC81') / decode(X'00') to keep the fixture ASCII.
  Verified these discriminate: adding them to string_translate_enabled.sql,
  which opts into the native path, fails.
- to_csv.sql gains options coverage (`sep`, and `timestampFormat`/`dateFormat`
  over date and timestamp fields), which the dispatcher path had nowhere.
- CometCsvExpressionSuite "to_csv - default options" drops the
  allowIncompatible=true wrapper and covers c6 Double, c10 Timestamp and c11
  TimestampNTZ, which it previously had to skip. It now tests what its name
  says.

Two field types are deliberately left unasserted, both Spark behavior rather
than Comet's, confirmed against Spark 3.5 with Comet disabled:

- Binary: Spark's CSV converter renders it with Java's default toString, e.g.
  "[B@731af74", an identity hash that differs between any two evaluations, so
  it is not comparable by any engine including Spark against itself.
- Complex types: a non-null array/map/struct renders as
  "org.apache.spark.sql.vectorized.ColumnarArray@1ada50f0" and any null complex
  value throws NullPointerException inside Spark's UnsafeWriter.write. Before
  this change these schemas fell back to Spark; now they reach the dispatcher,
  which runs the same Spark code, so the observable result is unchanged.
@andygrove

Copy link
Copy Markdown
Member Author

@mbutrovich all six threads addressed in 26994c9, details inline. You were right about the central point, and taking it shrank the PR considerably.

The whole serde diff is now two mixins. CodegenDispatchFallback already expresses exactly this routing, and both objects' original getSupportLevel declarations were already the correct classifications, so the hand-rolled branching is gone:

  • CometStringTranslate is back to CometScalarFunction[StringTranslate]("translate") plus the mixin — "translate" lives in one place again, and the opted-in logWarning comes back.
  • CometStructsToCsv is back to the original three-way getSupportLevel plus the mixin. getUnsupportedReasons is restored, the duplicated nativeSupported predicate and its two-call-site sync hazard are gone, and the missing logDebug you flagged separately comes for free from the framework's Unsupported arm. I also dropped CometCodegenDispatch as the base, since it is for expressions with no native path at all.

Tests. string_translate.sql gains the two rows that actually distinguish the paths, and I verified they discriminate rather than pass vacuously: adding them to string_translate_enabled.sql, which opts into the native path, fails. to_csv.sql gains sep and timestampFormat/dateFormat coverage. CometCsvExpressionSuite's "to_csv - default options" drops the allowIncompatible wrapper and now covers c6 Double, c10 Timestamp and c11 TimestampNTZ.

Two things I could not assert, both Spark behavior — verified with spark.comet.enabled=false:

  • c13 Binary: Spark's CSV converter renders it via Java's default toString(), e.g. [B@731af74. With c13 included, every row mismatches and the only differing token is that identity hash — the double and both timestamp columns match exactly. Not assertable by any engine, Spark included.
  • Complex types: non-null values render as org.apache.spark.sql.vectorized.ColumnarArray@1ada50f0 / ColumnarRow@... / ColumnarMap@..., and any null complex value throws NullPointerException in UnsafeWriter.write, with no Comet frames. So to_csv over complex types is either an identity hash or a crash in Spark itself, and there is no value-parity test to write. I documented this in the fixture instead of pinning garbage.

Neither is a regression here: those schemas previously fell back to Spark and now reach the dispatcher, which runs the same doGenCode, so the observable result is identical — the change only decides whether the rest of the projection stays native. Say the word if you want a separate issue tracking the complex-type behavior.

Verified on spark-3.5: to_csv.sql, string_translate.sql, string_translate_enabled.sql and both CometCsvExpressionSuite tests pass; spotless and scalastyle clean.

One unrelated note: this branch is behind main, which has since added an Implementation column to expressions.md. The two Notes cells this PR changes are still accurate, but the docs table will need regenerating after a rebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants