Guest User

Untitled

a guest
Jul 19th, 2018
101
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.23 KB | None | 0 0
  1. Example code under test:
  2.  
  3. `CExpressionPreprocessor::PexprRemoveSuperfluousOuterRefs`
  4. is a function which takes as input an expression representing a SQL query and returns an expression representing a SQL query which it has either processed or left alone.
  5. The specific intent of the function is to remove references to columns which are not required.
  6. If the input expression has certain characteristics, then a transformation is applied to it.
  7. In this case, those characteristics are the presence of a subquery which has an ORDER BY or GROUP BY clause containing a reference to a column from a table that is from outside of the subquery. According to SQL semantics, the query with and without those columns is equivalent, and, by removing that reference, it makes it more likely that this expression will be eligible for later tranformations.
  8.  
  9. In order to test that this step is correctly transforming an expression according to the intended logic, I want to create a test input expression and compare either the entire output expression to an "expected" output or assert on various characteristics of the output expression.
  10.  
  11. Let's take an example input expression. The serialized representation of an example input expression is as follows:
  12.  
  13. ```
  14. +--CLogicalSelect
  15. |--CLogicalGet "t" ("t"), Columns: ["a" (0), "b" (1), "ctid" (2), "xmin" (3), "cmin" (4), "xmax" (5), "cmax" (6), "tableoid" (7), "gp_segment_id" (8)] Key sets: {[2,8]}
  16. +--CScalarSubqueryAny(=)["count" (18)]
  17. |--CLogicalGbAgg( Global ) Grp Cols: ["j" (10), "b" (1)][Global], Minimal Grp Cols: [], Generates Duplicates :[ 0 ]
  18. | |--CLogicalGet "s" ("s"), Columns: ["i" (9), "j" (10), "ctid" (11), "xmin" (12), "cmin" (13), "xmax" (14), "cmax" (15), "tableoid" (16), "gp_segment_id" (17)] Key sets: {[2,8]}
  19. | +--CScalarProjectList
  20. | +--CScalarProjectElement "count" (18)
  21. | +--CScalarAggFunc (count , Distinct: false , Aggregate Stage: Global)
  22. | +--CScalarIdent "i" (9)
  23. +--CScalarIdent "a" (0)
  24. ```
  25.  
  26. Each component of this expression tree is a node of type `CExpression`. There is a class, CExpression.h, which prescribes the characteristics for such an expression.
  27. Each expression node has an array of child nodes saved in a member variable `m_pdrgpexpr`.
  28.  
  29. Each node also has a type expressed as its operator type. Valid operator types are logical, physical, scalar, and patterns. `COperator.h` describes a valid operator.
  30. The operator type is saved in a member variable in the expression `m_pop`
  31. An expression also has what are called derived relational properties--saved in `m_pdprel`. Some of these are displayed above--most significantly for this example, the columns -- e.g. `"a" (0)`
  32.  
  33. I would like to test that when I call `CExpressionPreprocessor::PexprRemoveSuperfluousOuterRefs` on an expression which has a `CScalarSubquery*` with a `CLogicalGbAgg` as a child having at least one outer reference that that outer reference is eliminated and the output expression has only columns from the table referenced in my subquery.
  34. The serialized output expression that I would be expecting to come out of the input above is
  35.  
  36. ```
  37. +--CLogicalSelect
  38. |--CLogicalGet "t" ("t"), Columns: ["a" (0), "b" (1), "ctid" (2), "xmin" (3), "cmin" (4), "xmax" (5), "cmax" (6), "tableoid" (7), "gp_segment_id" (8)] Key sets: {[2,8]}
  39. +--CScalarSubqueryAny(=)["count" (18)]
  40. |--CLogicalGbAgg( Global ) Grp Cols: ["j" (10)][Global], Minimal Grp Cols: ["j" (10)], Generates Duplicates :[ 0 ]
  41. | |--CLogicalGet "s" ("s"), Columns: ["i" (9), "j" (10), "ctid" (11), "xmin" (12), "cmin" (13), "xmax" (14), "cmax" (15), "tableoid" (16), "gp_segment_id" (17)] Key sets: {[2,8]}
  42. | +--CScalarProjectList
  43. | +--CScalarProjectElement "count" (18)
  44. | +--CScalarAggFunc (count , Distinct: false , Aggregate Stage: Global)
  45. | +--CScalarIdent "i" (9)
  46. +--CScalarIdent "a" (0)
  47. ```
  48. In order to construct a real input for a test of this step, I need to make real nodes for every node in the expression. This means that, even though I only care about the specific details of one of the nodes, I have to write dozens of lines of code to construct the rest of the expression for it to be a valid input to this function.
  49.  
  50. This is cumbersome for the developer.
  51.  
  52. One strategy that we have used so far for testing is to use a SQL query as the test input and assert that there is no diff in the plan that is produced.
  53. This is problematic because dozens of transformations are performed on an expression as part of planning, so if there is a difference in the plan, it is not clear which transformation or processing step led to that diff.
  54. This is also problematic because the input may have to go through other transformations from SQL before arriving at this function. So, an actual test input would not go directly from being compiled from SQL into an expression to having this function called on it.
  55. Even if this function was called directly after parsing and compilation to an expression, I would have to devise a SQL query which produces the correct input, and, if the code which parses and compiles the SQL query into an expression changes, my test would fail.
  56.  
  57. So, the first question is how do we test this single processing step in isolation.
  58.  
  59. Almost every escalation involves tracking down the bug manually in the code. This usually means finding the exact processing step which did the wrong thing. We usually do this by setting breakpoints and inspecting the expression tree at various spots throughout the code. Because we have discrete processing steps with input and output, it seems like we could more easily test this than using this strategy.
  60. This is data in/data out software, so testing it should be possible
  61.  
  62. Another strategy we have used is to create helper functions which make different specific nodes. For example, we have a helper function `CTestUtils::PexprLogicalGet` which creates a `CLogicalGet` similar to the one in the above serialized expression. This is helpful, however, it requires you to construct several inputs--for example, a real `CTableDescriptor` which is a representation of a table and requires a cascade of other inputs to be created in order to make it. The helper functions have several problems:
  63. 1. they still require the developer to make and specify many real inputs which is time consuming and painful
  64. 2. when there is a particular part of the input that I would like to make real or to specify, helper functions that make default values often don't have a good way for me to inject the details I do care about
  65. 3. they still require a large and elaborate test setup to take care of add ref'ing and memory management
  66. 4. It is nearly impossible to create inputs to test processing steps which take bigger and more elaborate input expressions
  67. I find that I end up creating a new helper function every time I want to write a test for a processing step to create the input, which makes it less of a helper function and more of a code obfuscation.
  68. I, as a developer, want the helper function making my test input to take care of making all of the parts of the test input which I don't care about and are not under test--or, more specifically, I want to be able to test that this production code works as intended while only having to create the objects under test.
  69.  
  70. This felt like it would be possible to solve with mocking. I have done a POC for mocking `CExpression` and will describe how it went.
  71.  
  72. Caveats about the prototype:
  73. - I didn't use a mocking framework, as I wanted to drive out the basic concepts, so the code could not be used in production, as it required me to make many of the functions in CExpression virtual.
  74. - I had my mocks inherit from the real classes instead of re-parenting the real classes to avoid a huge change to the code in my prototype
  75. - I ended up using several hacks to work around current specifics of the code base which can be changed, should we decide to go this route (for example, I made methods like the actual step under test `PexprRemoveSuperfluousOuterRefs` public)
  76. - Also, my mock's functions which have different behavior than the real functions only work for my very specific use case and would have to be generalized
  77. - I used some of the current test setup which obfuscates the details around memory management
  78. - I didn't refactor any code -- including where I added my test --
  79. I think that many of the above points are very important to address in the future, but my current question is even more basic: basically, with this mocking method, 1) am I even putting the code under test correctly and 2) will this make it easier to write valid test cases than either a) current state helper functions or b) other testing methods
  80.  
  81. I wrote a test which involved the following setup:
  82. - Standard memory management and metadata creation setup
  83. Real inputs components created:
  84. `CLogicalGbAgg`
  85. `CColRefSet`
  86. Mocks created:
  87. `CExpressionMock`
  88. `CDrvdPropRelationalMock`
  89.  
  90. I make an instance of my mock expression and call `PexprRemoveSuperfluousOuterRefs` on it. Then I assert that the length of the column references in the output of calling that function is as expected (from 2 to 1).
  91.  
  92. The actual mock ends up working like this in the production code:
  93. ```
  94. COperator::EOperatorId eopid = pop->Eopid();
  95. ```
  96. Gets the operator id of my mock expression which is the id of the real `CLogicalGbAgg` that I made for my test input.
  97. ```
  98. (pop->FLogical() && CUtils::FHasOuterRefs(pexpr));
  99. ```
  100. Returns true because my real `CLogicalGbAgg` is a logical operator and my mock expression returns a real `CColRefSet` when `PcrsOuter()` is called on it.
  101.  
  102. That means I fall into `if (fHasOuterRefs)` and the nested `else if (COperator::EopLogicalGbAgg == eopid)`
  103. This next line exercises the mock expression and the mock derived relational properties
  104. ```
  105. CColRefSet *pcrsOuter = Pdprel(pexpr->PdpDerive())->PcrsOuter(pmp);
  106. ```
  107.  
  108. `PdpDerive()` when called on my mock expression will create a mock `CDrvdPropRelational`.
  109. These mock derived relational properties return a real `CColRefSet` when I call `PcrsOuter()` on them.
  110. In the next line:
  111. ```
  112. CLogicalGbAgg *popAgg = CLogicalGbAgg::PopConvert(pop);
  113. ```
  114. my real `CLogicalGbAgg` is obtained.
  115. Now, I call
  116. ```
  117. DrgPcr *pdrgpcr = CUtils::PdrgpcrExcludeColumns(pmp, popAgg->Pdrgpcr(), pcrsOuter);
  118. ```
  119. With my outer references and my logical group by agg operator.
  120. `CUtils::PdrgpcrExcludeColumns` wil return a copy of the columns, excluding the column "b" which is from outside of my subquery.
  121.  
  122. As part of constructing my real `CLogicalGbAgg`, I will have set the minimal columns required (based on functional dependencies).
  123. Next I use my fake `CExpression` in order to get a fake child which has a fake arity so that I can meet the condition that the project list is not empty.
  124. Then I will access the minimal colrefset which I constructed my groupby with
  125. ```
  126. DrgPcr *pdrgpcrMinimal = popAgg->PdrgpcrMinimal();
  127. ```
  128. I'll exclude my outer referencing columns from it
  129. ```
  130. pdrgpcrMinimal = CUtils::PdrgpcrExcludeColumns(pmp, pdrgpcrMinimal, pcrsOuter);
  131. ```
  132. In order to make sure that I have columns that are not actually outer references in my list of group by columns.
  133.  
  134. Then I make a new `CLogicalGbAgg` which has the new set of column references with the superfluous outer references elimintated.
  135. At the bottom of the function, before creating a new expression containing the new `CLogicalGbAgg`, we recursively process the children of my expression in order to remove their superfluous outer references.
  136. ```
  137. const ULONG ulArity = pexpr->UlArity();
  138. DrgPexpr *pdrgpexprChildren = GPOS_NEW(pmp) DrgPexpr(pmp);
  139. for (ULONG ul = 0; ul < ulArity; ul++)
  140. {
  141. CExpression *pexprChild = PexprRemoveSuperfluousOuterRefs(pmp, (*pexpr)[ul]);
  142. pdrgpexprChildren->Append(pexprChild);
  143. }
  144. ```
  145. Because our test case only requires us to remove outer references from the parent expression, we want this code to be a no-op.
  146. We do create our new output expression with the children we found while looping. So, this actually will work fine with our mock--as none of our children will meet the condition to be processed by this step.
  147. Then we create our output expression.
  148. ```
  149. return GPOS_NEW(pmp) CExpression(pmp, pop, pdrgpexprChildren);
  150. ```
  151.  
  152. So, my questions are, with my use of a mock to create this test
  153. 1. Am I actually exercising the code in a way that is meaningful?
  154. 2. Is this the best way to test this code? And, if not, what is?
  155.  
  156.  
  157. Other strategies I've considered are:
  158. - Using XML-style string matching on serialized versions of the code
  159. - Steps:
  160. - Parse into an expression an XML-style test input and expected output
  161. - Call the function on the parsed input
  162. - Serialize the output of calling the function
  163. - String compare the output and expected output
  164. - Challenges
  165. - The string input has to be formatted perfectly and is therefore brittle
  166. - Developers still have to "care about"/come up with all of the parts of the expression
  167. - Developers should write code to test a function that takes code objects as input and not edit XML strings
  168. - If the parsing or serialization code breaks, these tests would break
  169. - String matching doesn't allow us to assert on specific aspects of the test output, which makes it less effective as a way of documenting the code
  170. - Any code that changes any other part of the expression would cause the test to fail (which is most of the code in the code base)
  171. - Writing a DSL to generate real input expressions which has sensible defaults for all nodes and attributes not specified by the developer
  172. - Steps
  173. - ?
  174. - Challenges
  175. - Sounds hard
  176. - Pros
  177. - Easy to make new tests
  178. - Expressions could be arbitrarily complicated much more easily
  179. - Change the processing steps production code to make it easier to test without mocks
  180. - Steps
  181. - change individual processing steps to mock out function calls that would require more complicated inputs
  182. - unit-test individual functions called in the processing step `CUtils::PdrgpcrExcludeColumns`
  183. - then test the overall expression transformation only for "happy path" simple case
  184. - Challenges
  185. - hard to generalize
  186. - unclear mandate
  187. - inconsistent ability to apply
  188. - breaking up and de-centralizing these steps will make it extremely difficult to isolate the cause of bugs using breakpoints and a debugger, as we do now
  189. - Harder to test that the characteristics of more complicated expressions are actually identified
  190. - for example, identify a `CColRefSet` which includes an outer reference which is in a nested subquery is correctly processed
  191. - Pros
  192. - developers can use their judgment about how to test steps they add
  193. - Mocks
  194. - Steps
  195. - Challenges
  196. - not sure how easy it will be to generate arbitrarily complicated expressions
  197. - not sure how easy it will be to generalize the mock methods of CExpression to work for different types of expressions--maybe we should mock the operators and not the expression -- that is a lot of mocks
  198. - recursion is a challenge -- all of the processing steps recurse on the children. if we are using mock objects to make sure that we only process the parts of the expression that we feel like spec'ing out, then we run into the problem where recursion will force us to have to construct the whole expression. How do we get recursion to work with mocking?
  199. - TODO: explain this better: the arbitrary separation betweeen COperator and CExpression also is a source of problems. If we mock CExpression and then proceed to define a mock UlArity() function, for example, even if we know that for a particular type of operator, there is an expected arity and we should be able to return a const value in our mock code, we cannot do that because UlArity() will not be called polymorphically -- it is called on a class which is not a parent of the particular operator type. This means that if we want UlArity to return a different value for different types of operators, we would have to add a large switch-case to the UlArity() mock method which switches on the operator type. This is in contrast to being able to do it in the specific operator's mock. If we combined COperator and CExpression, this may be possible
  200. - Pros
  201. - test the code directly
  202. - it is clear what you care about
  203.  
  204.  
  205. What ideas do people have about the above or about completely different strategies?
  206.  
  207. One important point is that these processing steps are one kind of transformation we do. We also implement a large number of other transformations that do not take a simple expression as input but instead take a memo-ized version of many expressions organized into groups. This code is particularly challenging to test because these different expressions which are memo-ized represent many expressions which do not get picked to be the final plan. This means you cannot test that they produce the right alternatives with SQL. Inherently, many of the test cases you would choose involve the production of expressions which would be eliminated by later steps.
  208. The goal in giving this less complicated example is to come up with a testing strategy that could be extended to these steps.
  209.  
  210. More caveats:
  211. - We do have tests for various processing steps which take the time to create real inputs and compare them to outputs--it's just time-consuming to create new ones
  212. - We have a robust framework for integration testing query planning which uses a special format called "minidump" to make the entire query planning process data in/data out and allows us to detect any difference in query plan produced for a given query. This means we already have parsing and serialization defined for all of the nodes we are discussing.
Add Comment
Please, Sign In to add comment