Skip to content
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

sql: add support for subqueries with ANY/SOME/ALL operations #18094

Merged
merged 6 commits into from
Sep 6, 2017

Conversation

richardwu
Copy link
Contributor

Previously we only supported arrays for ANY and ALL operations:

SELECT 1 = ANY(ARRAY[1, 2, 3]);

SELECT 1 = ANY(ARRAY[1, 2, 3]);
+--------------------------+
| 1 = ANY (ARRAY[1, 2, 3]) |
+--------------------------+
| true                     |
+--------------------------+
(1 row)

Time: 508.392µs

This PR allows us to use subqueries as well in the RHS of the predicate:

SELECT 1 = ANY(SELECT * FROM generate_series(1,3));
+--------------------------------+
| 1 = ANY (SELECT * FROM generate_series(1, 3)) |
+--------------------------------+
| true                           |
+--------------------------------+
(1 row)

Time: 648.021µs

Fixes #17662

@richardwu richardwu added the first-pr Use to mark the first PR sent by a contributor / team member. Reviewers should be mindful of this. label Aug 31, 2017
@richardwu richardwu requested review from justinj, knz, vivekmenezes and a team August 31, 2017 19:06
@CLAassistant
Copy link

CLAassistant commented Aug 31, 2017

CLA assistant check
All committers have signed the CLA.

@cockroach-teamcity
Copy link
Member

This change is Reviewable


if all {
if !allTrue {
if res && any {
Copy link
Contributor Author

@richardwu richardwu Aug 31, 2017

Choose a reason for hiding this comment

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

I optimized this loop through each Datum in our slice of Datums such that it short circuits/early terminates when:

  1. ANY: result is encountered where res = true (formally, there exists a right such that the predicate evaluates to true, "any" is true)
  2. ALL: result is encountered where res = false (there exists a right such that the predicate evaluates to false, not all are true)

This results in statements such as this

SELECT 1 = ALL(SELECT * FROM generate_series(1,1000000))

to terminate after evaluating 1 = 2 instead of iterating until the end.

@justinj
Copy link
Contributor

justinj commented Aug 31, 2017

This looks great so far! I left some comments, but @knz should probably take a look as well!


Review status: 0 of 6 files reviewed at latest revision, 6 unresolved discussions, some commit checks pending.


pkg/sql/logictest/testdata/logic_test/suboperators, line 1 at r1 (raw file):

# LogicTest: default parallel-stmts distsql

So thorough 👍 could we also have a couple tests involving NULLs?


pkg/sql/parser/eval.go, line 1742 at r1 (raw file):

	any := !all
	sawNull := false
	for _, elem := range right {

Perhaps we could just branch once at the top for any vs. all and then have a different for within each branch (maybe pulling each into helper functions)? It might simplify this a little and remove the need for the explanatory short-circuiting comments.


pkg/sql/parser/eval.go, line 2726 at r1 (raw file):

	if op.hasSubOperator() {
		// Branch to different helper comparison functions
		// depending on whether a subquery or an array follows

nit: we generally put periods at the end of comments (also applies to some other comments here), also, is this comment accurate? we don't seem to branch to different functions, just different ways of extracting the Datums


pkg/sql/parser/eval.go, line 2733 at r1 (raw file):

			datums = array.Array
		}
		// Type checking in parser ensures rightType is either TypeTuple or TypeArray

consider adding a panic in an else clause with this as the error message instead? if this is violated we probably don't want to continue.


pkg/sql/parser/type_check.go, line 1011 at r1 (raw file):

		rightUnwrapped := UnwrapType(rightReturn)
		switch rightUnwrapped.(type) {

if you replace this with a type switch you won't have to do the explicit casts below.


Comments from Reviewable

@knz
Copy link
Contributor

knz commented Aug 31, 2017

I'll let you process and iterate with Justin first. Once you've settled together I'll do a round too. I don't think it's useful to have 2+ people reviewing at the same time for now. Let me know.

Please however do respect the rule 1 concern = 1 commit, see my comment below.


Review status: 0 of 6 files reviewed at latest revision, 6 unresolved discussions, all commit checks successful.


pkg/sql/parser/eval.go, line 1759 at r1 (raw file):

Previously, richardwu (Richard Wu) wrote…

I optimized this loop through each Datum in our slice of Datums such that it short circuits/early terminates when:

  1. ANY: result is encountered where res = true (formally, there exists a right such that the predicate evaluates to true, "any" is true)
  2. ALL: result is encountered where res = false (there exists a right such that the predicate evaluates to false, not all are true)

This results in statements such as this

SELECT 1 = ALL(SELECT * FROM generate_series(1,1000000))

to terminate after evaluating 1 = 2 instead of iterating until the end.

Please split this change into a different commit. Add tests that involve NULL in the subquery.


Comments from Reviewable

@richardwu
Copy link
Contributor Author

Review status: 0 of 6 files reviewed at latest revision, 6 unresolved discussions, some commit checks pending.


pkg/sql/logictest/testdata/logic_test/suboperators, line 1 at r1 (raw file):

Previously, justinj (Justin Jaffray) wrote…

So thorough 👍 could we also have a couple tests involving NULLs?

Done.


pkg/sql/parser/eval.go, line 1742 at r1 (raw file):

Previously, justinj (Justin Jaffray) wrote…

Perhaps we could just branch once at the top for any vs. all and then have a different for within each branch (maybe pulling each into helper functions)? It might simplify this a little and remove the need for the explanatory short-circuiting comments.

As per @knz's comment, I will move this refactor to a separate PR.


pkg/sql/parser/eval.go, line 2726 at r1 (raw file):

Previously, justinj (Justin Jaffray) wrote…

nit: we generally put periods at the end of comments (also applies to some other comments here), also, is this comment accurate? we don't seem to branch to different functions, just different ways of extracting the Datums

Done.


pkg/sql/parser/eval.go, line 2733 at r1 (raw file):

Previously, justinj (Justin Jaffray) wrote…

consider adding a panic in an else clause with this as the error message instead? if this is violated we probably don't want to continue.

I decided to propagate an internal PG error so we don't crash the instance.


pkg/sql/parser/type_check.go, line 1011 at r1 (raw file):

Previously, justinj (Justin Jaffray) wrote…

if you replace this with a type switch you won't have to do the explicit casts below.

Done.


Comments from Reviewable

@justinj
Copy link
Contributor

justinj commented Aug 31, 2017

This all looks good to me (modulo a squash and making sure the final commit name has the sql: prefix)! All yours @knz.


Review status: 0 of 6 files reviewed at latest revision, 1 unresolved discussion, some commit checks pending.


pkg/sql/parser/eval.go, line 1742 at r1 (raw file):

Previously, richardwu (Richard Wu) wrote…

As per @knz's comment, I will move this refactor to a separate PR.

sgtm 👍


pkg/sql/parser/eval.go, line 2733 at r1 (raw file):

Previously, richardwu (Richard Wu) wrote…

I decided to propagate an internal PG error so we don't crash the instance.

sounds reasonable. I don't think we log CodeInternalErrors to sentry like we do panics, but we probably should.


Comments from Reviewable

@knz
Copy link
Contributor

knz commented Aug 31, 2017

Reviewed 5 of 5 files at r1, 1 of 1 files at r2, 1 of 1 files at r3, 1 of 2 files at r4, 1 of 1 files at r5.
Review status: all files reviewed at latest revision, 4 unresolved discussions, some commit checks failed.


pkg/sql/parser/eval.go, line 2733 at r1 (raw file):

Previously, justinj (Justin Jaffray) wrote…

sounds reasonable. I don't think we log CodeInternalErrors to sentry like we do panics, but we probably should.

I think we do actually -- or if we don't there's an issue already for doing so, scheduled for 1.2. all good.


pkg/sql/parser/type_check.go, line 998 at r1 (raw file):

		cmpTypeLeft = leftTyped.ResolvedType()

		// Try to type the right expression as an Array of the left's type.

"as an array" - capital not needed


pkg/sql/parser/type_check.go, line 1017 at r5 (raw file):

			// Subqueries are expected to return 1 column of values
			// (see planner.analyzeExpr in analyze.go).
			cmpTypeRight = rightUnwrapped[0]

Woah, hold your horses. We can produce tuples in SQL in other ways than using queries!
What do you think should happen with 1 = ANY(3,1,3) ? Right now your code says "false" but I think it should be "true". Moreover, we can generate zero-tuples with "ROW()", e.g. 1 = ANY(ROW()), I fear you're getting "array out of bounds" panic here.

I don't think you'll be getting away without special casing subqueries out of other kinds of tuples.


Comments from Reviewable

@justinj
Copy link
Contributor

justinj commented Aug 31, 2017

Review status: all files reviewed at latest revision, 4 unresolved discussions, some commit checks failed.


pkg/sql/parser/type_check.go, line 1017 at r5 (raw file):

Previously, knz (kena) wrote…

Woah, hold your horses. We can produce tuples in SQL in other ways than using queries!
What do you think should happen with 1 = ANY(3,1,3) ? Right now your code says "false" but I think it should be "true". Moreover, we can generate zero-tuples with "ROW()", e.g. 1 = ANY(ROW()), I fear you're getting "array out of bounds" panic here.

I don't think you'll be getting away without special casing subqueries out of other kinds of tuples.

Ah, you're right, sorry for missing that @richardwu!


Comments from Reviewable

@richardwu
Copy link
Contributor Author

Review status: 0 of 7 files reviewed at latest revision, 4 unresolved discussions, some commit checks pending.


pkg/sql/parser/type_check.go, line 998 at r1 (raw file):

Previously, knz (kena) wrote…

"as an array" - capital not needed

Done.


pkg/sql/parser/type_check.go, line 1017 at r5 (raw file):

Previously, justinj (Justin Jaffray) wrote…

Ah, you're right, sorry for missing that @richardwu!

Done.


Comments from Reviewable

@richardwu
Copy link
Contributor Author

richardwu commented Sep 1, 2017

So I ran into a few failed tests that resulted from our use of type checking on sql.subquery (which breaks now that we wrap sql.subquery with parser.SubqueryPlaceholder).

The "lifetime" of the new parser.SubqueryPlaceholder begins at replaceSubqueries and lives until TypeCheck (where TypeCheck is delegated to the nested sql.subquery) in analyzeExpr https://github.com/cockroachdb/cockroach/blob/1ae8e374f9c8bbeae2e41cfb4375b2c5a7b42ccd/pkg/sql/analyze.go#L1642#L1667.

I tried my best to go through all the instances of type checking for sql.subquery during the lifespan of parser.SubqueryPlaceholder https://github.com/cockroachdb/cockroach/pull/18094/files#diff-06230dfb99f17a44b32848b107b7fd56R310 and https://github.com/cockroachdb/cockroach/pull/18094/files#diff-06230dfb99f17a44b32848b107b7fd56R481, but I think it ought be best if someone with more knowledge about this part of code comment on this approach.

@knz
Copy link
Contributor

knz commented Sep 1, 2017

Reviewed 1 of 6 files at r6, 1 of 1 files at r7, 1 of 1 files at r10, 5 of 5 files at r11.
Review status: all files reviewed at latest revision, 10 unresolved discussions, all commit checks successful.


pkg/sql/subquery.go, line 333 at r11 (raw file):

	switch expr.(type) {
	// We already replaced this one; do nothing.
	case *subquery, *parser.SubqueryPlaceholder:

I don't think you can observe *subquery any more.


pkg/sql/update.go, line 310 at r11 (raw file):

					currentUpdateIdx++
				}
			case *subquery, *parser.SubqueryPlaceholder:

I think because of your change it becomes impossible to encounter a naked *subquery here. Can you confirm?


pkg/sql/update.go, line 481 at r11 (raw file):

			n := -1
			switch t := expr.Expr.(type) {
			case *subquery, *parser.SubqueryPlaceholder:

ditto


pkg/sql/logictest/testdata/logic_test/suboperators, line 327 at r11 (raw file):

SELECT 1 = ANY ROW()

query error unsupported comparison operator: 1 = ANY \(1, 2, 3\)

I fail to agree here -- it seems to me that we can compare a value with the members of a tuple, when the tuple values match in type.


pkg/sql/parser/expr.go, line 737 at r11 (raw file):

}

func (s *SubqueryPlaceholder) String() string { return s.Sq.String() }

This String method does not belong here. Place it at the end of expr.go with the others.


pkg/sql/parser/expr.go, line 749 at r11 (raw file):

// TypeCheck implements the Expr interface.
func (s *SubqueryPlaceholder) TypeCheck(ctx *SemaContext, desired Type) (TypedExpr, error) {
	return s.Sq.TypeCheck(ctx, desired)

Unless you do the deferred construction thing I explained earlier, this method should do return s, nil.

The entire Placeholder must implement the Expr interface, which in turn means it also needs an Eval and ResolvedType methods. These do recurse into Sq, to be provided by sql.subquery.


pkg/sql/parser/type_check.go, line 1019 at r11 (raw file):

			// Subqueries are expected to return 1 column of values
			// (see planner.analyzeExpr in analyze.go).
			if _, ok := right.(*SubqueryPlaceholder); !ok {

You need two separate pieces of logic, one for tuples generated by non-subqueries (where the RHS is the entire tuple) and one for tuples geneerated by subqueries.


Comments from Reviewable

@knz
Copy link
Contributor

knz commented Sep 1, 2017

Review status: all files reviewed at latest revision, 11 unresolved discussions, all commit checks successful.


pkg/sql/parser/expr.go, line 734 at r11 (raw file):

// during type checking.
type SubqueryPlaceholder struct {
	Sq Expr

Sq TypedExpr btw, the subquery already has its types.


Comments from Reviewable

@richardwu
Copy link
Contributor Author

pkg/sql/logictest/testdata/logic_test/suboperators, line 327 at r11 (raw file):

Previously, knz (kena) wrote…

I fail to agree here -- it seems to me that we can compare a value with the members of a tuple, when the tuple values match in type.

So I was always under the impression that we try to follow Postgres semantics as much as possible (Postgres errors with "right hand side must be an array"). Do we want to introduce this feature of being able to use primitive tuples with ANY/ALL operations? If that's the case, we can defer the usage of a SubqueryPlaceholder and simply check for >=1 types for tuple right expressions.


Comments from Reviewable

@richardwu richardwu force-pushed the any-all-operations branch 2 times, most recently from dd19920 to 8fb4da8 Compare September 1, 2017 22:15
@richardwu
Copy link
Contributor Author

pkg/sql/subquery.go, line 333 at r11 (raw file):

Previously, knz (kena) wrote…

I don't think you can observe *subquery any more.

I have decided to not introduce the SubqueryPlaceholder in this PR since it is not strictly required (since we are supporting tuples with ANY/ALL, we do not need to special case subqueries as they evaluate to tuples).


Comments from Reviewable

@richardwu
Copy link
Contributor Author

Review status: 2 of 6 files reviewed at latest revision, 11 unresolved discussions, some commit checks pending.


pkg/sql/parser/type_check.go, line 1019 at r11 (raw file):

Previously, knz (kena) wrote…

You need two separate pieces of logic, one for tuples generated by non-subqueries (where the RHS is the entire tuple) and one for tuples geneerated by subqueries.

Ditto above comment


Comments from Reviewable

@richardwu
Copy link
Contributor Author

Review status: 2 of 6 files reviewed at latest revision, 11 unresolved discussions, some commit checks pending.


pkg/sql/update.go, line 310 at r11 (raw file):

Previously, knz (kena) wrote…

I think because of your change it becomes impossible to encounter a naked *subquery here. Can you confirm?

Done.


pkg/sql/update.go, line 481 at r11 (raw file):

Previously, knz (kena) wrote…

ditto

Done.


pkg/sql/parser/expr.go, line 734 at r11 (raw file):

Previously, knz (kena) wrote…

Sq TypedExpr btw, the subquery already has its types.

Done.


pkg/sql/parser/expr.go, line 737 at r11 (raw file):

Previously, knz (kena) wrote…

This String method does not belong here. Place it at the end of expr.go with the others.

Done.


pkg/sql/parser/expr.go, line 749 at r11 (raw file):

Previously, knz (kena) wrote…

Unless you do the deferred construction thing I explained earlier, this method should do return s, nil.

The entire Placeholder must implement the Expr interface, which in turn means it also needs an Eval and ResolvedType methods. These do recurse into Sq, to be provided by sql.subquery.

Done.


Comments from Reviewable

@knz
Copy link
Contributor

knz commented Sep 2, 2017

It's nice to see how unifying the tuple and subquery cases also simplify the code. I like where this is headed at last.

One last bug to correct and you should be good to go.


Reviewed 6 of 6 files at r12.
Review status: all files reviewed at latest revision, 5 unresolved discussions, all commit checks successful.


pkg/sql/parser/type_check.go, line 1000 at r12 (raw file):

		// Try to type the right expression as an array of the left's type.
		// If right is an sql.subquery Expr, it should already be typed.

Add:
// TODO(richardwu): If right is a subquery, we should really propagate the left type as a desired type for the result column.


pkg/sql/parser/type_check.go, line 1029 at r12 (raw file):

	var ok bool
	for _, cmpTypeRight := range cmpTypesRight {
		if fn, ok = ops.lookupImpl(cmpTypeLeft, cmpTypeRight); !ok {

This is incorrect, and the following statement will demonstrate it is incorrect by crashing the server:
SELECT 4 = any(1, '2', 3);

What you need to do:

  • revert back cmpTypesRight []Type to cmpTypeRight Type above
  • above in the TTuple case, check that all the tuple element types are identical, and save that (unique) type into cmpTypeRight
  • restore the previous code that was looking up fn

Comments from Reviewable

@knz
Copy link
Contributor

knz commented Sep 2, 2017

Review status: all files reviewed at latest revision, 5 unresolved discussions, all commit checks successful.


pkg/sql/parser/type_check.go, line 1000 at r12 (raw file):

Previously, knz (kena) wrote…

Add:
// TODO(richardwu): If right is a subquery, we should really propagate the left type as a desired type for the result column.

Also, change this to use typeCheckSameTypedTupleExprs if the right side is a tuple, so as to maximize the chance the tuple has the right element types.


pkg/sql/parser/type_check.go, line 1029 at r12 (raw file):

Previously, knz (kena) wrote…

This is incorrect, and the following statement will demonstrate it is incorrect by crashing the server:
SELECT 4 = any(1, '2', 3);

What you need to do:

  • revert back cmpTypesRight []Type to cmpTypeRight Type above
  • above in the TTuple case, check that all the tuple element types are identical, and save that (unique) type into cmpTypeRight
  • restore the previous code that was looking up fn

Also, add a test -- the example I gave should properly return an error.


Comments from Reviewable

@richardwu
Copy link
Contributor Author

pkg/sql/parser/type_check.go, line 1029 at r12 (raw file):

Previously, knz (kena) wrote…

Also, add a test -- the example I gave should properly return an error.

So I played around with the current implementation and it does correctly return an error for the statement SELECT 4 = any(1, '2', 3) (it notices it can't compare an int with a string).

If I were to check all tuple element types to be identical, then the following statement would return a type mismatch error SELECT 4 = any(1, 2.5). This will be inconsistent with how we interpret tuples with IN, that is

SELECT 1 IN (1, 2.5);
+------------------+
| 1 IN (1, 2.5) |
+------------------+
| true             |
+------------------+
(1 row)

Time: 615.825µs

You can check out the logic test I've added with your example https://github.com/cockroachdb/cockroach/pull/18094/files#diff-7021787014e75d2e5d4600ab91b6475aR353.


Comments from Reviewable

@richardwu
Copy link
Contributor Author

pkg/sql/parser/type_check.go, line 1000 at r12 (raw file):

Previously, knz (kena) wrote…

Also, change this to use typeCheckSameTypedTupleExprs if the right side is a tuple, so as to maximize the chance the tuple has the right element types.

So I did some investigation with how our TypeCheck works, and it looks like TypeCheck does recurse down to Tuple and Array. In fact, Tuple.TypeCheck does type check every element of the tuple https://github.com/cockroachdb/cockroach/blob/16971acd9046d8e07a02d544a0b3262e7a2463a9/pkg/sql/parser/type_check.go#L737L748.

I've updated the statement to simply type check cmpTypeLeft (instead of TArray{cmpTypeLeft} which was wholly unnecessary) https://github.com/cockroachdb/cockroach/pull/18094/files#diff-f5c8f9ab30e6b17710413b03d2169349R1003.

I've verified we correctly type check expressions in the logs for both arrays and tuples

SELECT 1 = ANY (ARRAY[1,2] || 3)

// Before typing 
W170902 19:49:14.446858 311 sql/parser/type_check.go:1005  &parser.ParenExpr{Expr:(*parser.BinaryExpr)(0xc42064e090), typeAnnotation:parser.typeAnnotation{typ:parser.Type(nil)}}
// After typing
W170902 19:49:14.446942 311 sql/parser/type_check.go:1011  &parser.ParenExpr{Expr:(*parser.BinaryExpr)(0xc42064e090), typeAnnotation:parser.typeAnnotation{typ:parser.TArray{Typ:parser.tInt{}}}}

SELECT 1 = ANY((1, 2, 3.5))

// Before typing
W170902 19:49:48.748120 311 sql/parser/type_check.go:1005  &parser.ParenExpr{Expr:(*parser.Tuple)(0xc4208358c0), typeAnnotation:parser.typeAnnotation{typ:parser.Type(nil)}}
// After typing
W170902 19:49:48.748160 311 sql/parser/type_check.go:1011  &parser.ParenExpr{Expr:(*parser.Tuple)(0xc4208358c0), typeAnnotation:parser.typeAnnotation{typ:parser.TTuple{parser.tInt{}, parser.tInt{}, parser.tDecimal{}}}}

Comments from Reviewable

@knz
Copy link
Contributor

knz commented Sep 4, 2017

Review status: 4 of 6 files reviewed at latest revision, 5 unresolved discussions, some commit checks failed.


pkg/sql/parser/type_check.go, line 1029 at r12 (raw file):

Previously, richardwu (Richard Wu) wrote…

So I played around with the current implementation and it does correctly return an error for the statement SELECT 4 = any(1, '2', 3) (it notices it can't compare an int with a string).

If I were to check all tuple element types to be identical, then the following statement would return a type mismatch error SELECT 4 = any(1, 2.5). This will be inconsistent with how we interpret tuples with IN, that is

SELECT 1 IN (1, 2.5);
+------------------+
| 1 IN (1, 2.5) |
+------------------+
| true             |
+------------------+
(1 row)

Time: 615.825µs

You can check out the logic test I've added with your example https://github.com/cockroachdb/cockroach/pull/18094/files#diff-7021787014e75d2e5d4600ab91b6475aR353.

If you use typeCheckSameTypedTupleExprs you can use it to propagate the type on the left to all the elements on the right. It's ok if 1 = any(1, 2.5) fails, but 1::decimal = any(1, 2.5) should work.

Also check with Jordan what is going on with placeholders. Right now the tests fail.


Comments from Reviewable

@richardwu
Copy link
Contributor Author

pkg/sql/parser/type_check.go, line 1029 at r12 (raw file):

Previously, knz (kena) wrote…

If you use typeCheckSameTypedTupleExprs you can use it to propagate the type on the left to all the elements on the right. It's ok if 1 = any(1, 2.5) fails, but 1::decimal = any(1, 2.5) should work.

Also check with Jordan what is going on with placeholders. Right now the tests fail.

As discussed offline, typeCheckSameTypedTupleExprs is useful for multiple Tuple expressions, but does not check (let alone require) individual elements in a single Tuple expression.

I've written a helper function typeCheckAndRequireTupleElems (https://github.com/cockroachdb/cockroach/pull/18094/files#diff-f5c8f9ab30e6b17710413b03d2169349R912) to check and require individual elements of a tuple (using the existing typeCheckAndRequire helper function).

I've also updated logic tests for the correct behavior in @knz's comment.


Comments from Reviewable

@knz
Copy link
Contributor

knz commented Sep 6, 2017

Ok :lgtm:
Well done!


Reviewed 1 of 5 files at r13, 1 of 1 files at r17, 3 of 3 files at r18.
Review status: all files reviewed at latest revision, 5 unresolved discussions, all commit checks successful.


pkg/sql/parser/type_check.go, line 917 at r18 (raw file):

	for i, subExpr := range tuple.Exprs {
		// Require that the sub expression is equivalent (or may be inferred) to the required type.
		typedExpr, err := typeCheckAndRequire(ctx, subExpr, required, "TUPLE element")

lowercase "tuple"


Comments from Reviewable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
first-pr Use to mark the first PR sent by a contributor / team member. Reviewers should be mindful of this.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants