validation_message.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. # Copyright Materialize, Inc. and contributors. All rights reserved.
  2. #
  3. # Use of this software is governed by the Business Source License
  4. # included in the LICENSE file at the root of this repository.
  5. #
  6. # As of the Change Date specified in that file, in accordance with
  7. # the Business Source License, use of this software will be governed
  8. # by the Apache License, Version 2.0.
  9. from enum import Enum
  10. from typing import Any
  11. from materialize.output_consistency.execution.evaluation_strategy import (
  12. EvaluationStrategy,
  13. EvaluationStrategyKey,
  14. )
  15. from materialize.output_consistency.execution.sql_dialect_adjuster import (
  16. SqlDialectAdjuster,
  17. )
  18. from materialize.output_consistency.expression.expression import Expression
  19. from materialize.output_consistency.query.query_result import QueryExecution
  20. class ValidationErrorType(Enum):
  21. SUCCESS_MISMATCH = 1
  22. """Different outcome (success vs. error)"""
  23. ROW_COUNT_MISMATCH = 2
  24. """Different number of rows"""
  25. CONTENT_TYPE_MISMATCH = 3
  26. """Different data types in successful queries"""
  27. CONTENT_MISMATCH = 4
  28. """Different data in successful queries"""
  29. ERROR_MISMATCH = 5
  30. """Different error messages"""
  31. EXPLAIN_PLAN_MISMATCH = 6
  32. """Different explain plans"""
  33. def __str__(self) -> str:
  34. return self.name
  35. class ValidationMessage:
  36. """Either a `ValidationRemark`, `ValidationWarning`, or `ValidationError`"""
  37. def __init__(
  38. self,
  39. message: str,
  40. description: str | None = None,
  41. ):
  42. self.message = message
  43. self.description = description
  44. class ValidationRemark(ValidationMessage):
  45. def __init__(
  46. self,
  47. message: str,
  48. description: str | None = None,
  49. sql: str | None = None,
  50. ):
  51. super().__init__(message, description)
  52. self.sql = sql
  53. def __str__(self) -> str:
  54. remark_desc = f" ({self.description})" if self.description else ""
  55. query_desc = f"\n Query: {self.sql}" if self.sql else ""
  56. return f"{self.message}{remark_desc}{query_desc}"
  57. class ValidationWarning(ValidationMessage):
  58. def __init__(
  59. self,
  60. message: str,
  61. description: str | None = None,
  62. strategy: EvaluationStrategy | None = None,
  63. sql: str | None = None,
  64. ):
  65. super().__init__(message, description)
  66. self.strategy = strategy
  67. self.sql = sql
  68. def __str__(self) -> str:
  69. warning_desc = f": {self.description}" if self.description else ""
  70. strategy_desc = f" with strategy '{self.strategy}'" if self.strategy else ""
  71. query_desc = f"\n Query: {self.sql}" if self.sql else ""
  72. return f"{self.message}{strategy_desc}{warning_desc}{query_desc}"
  73. class ValidationErrorDetails:
  74. def __init__(
  75. self,
  76. strategy: EvaluationStrategy,
  77. value: Any,
  78. sql: str | None = None,
  79. sql_error: str | None = None,
  80. ):
  81. self.strategy = strategy
  82. self.value = value
  83. self.sql = sql
  84. self.sql_error = sql_error
  85. class ValidationError(ValidationMessage):
  86. def __init__(
  87. self,
  88. query_execution: QueryExecution,
  89. error_type: ValidationErrorType,
  90. message: str,
  91. details1: ValidationErrorDetails,
  92. details2: ValidationErrorDetails,
  93. description: str | None = None,
  94. col_index: int | None = None,
  95. concerned_expression: Expression | None = None,
  96. location: str | None = None,
  97. ):
  98. super().__init__(message, description)
  99. self.query_execution = query_execution
  100. self.error_type = error_type
  101. self.details1 = details1
  102. self.details2 = details2
  103. self.col_index = col_index
  104. if concerned_expression is not None:
  105. self.concerned_expression_str = concerned_expression.to_sql(
  106. SqlDialectAdjuster(), query_execution.query_template.uses_join(), True
  107. )
  108. self.concerned_expression_hash = concerned_expression.hash()
  109. else:
  110. self.concerned_expression_str = None
  111. self.concerned_expression_hash = None
  112. self.location = location
  113. def get_details_by_strategy_key(
  114. self,
  115. ) -> dict[EvaluationStrategyKey, ValidationErrorDetails]:
  116. return {
  117. details.strategy.identifier: details
  118. for details in [self.details1, self.details2]
  119. }
  120. def __str__(self) -> str:
  121. error_desc = f" ({self.description})" if self.description else ""
  122. location_desc = f" at {self.location}" if self.location is not None else ""
  123. expression_desc = (
  124. f"\nExpression: {self.concerned_expression_str}"
  125. if self.concerned_expression_str is not None
  126. else ""
  127. )
  128. strategy1_desc = f" ({self.details1.strategy})"
  129. strategy2_desc = f" ({self.details2.strategy})"
  130. value_and_strategy_desc = (
  131. f"\n Value 1{strategy1_desc}: '{self.details1.value}' (type: {type(self.details1.value)})"
  132. f"\n Value 2{strategy2_desc}: '{self.details2.value}' (type: {type(self.details2.value)})"
  133. )
  134. if self.error_type == ValidationErrorType.SUCCESS_MISMATCH:
  135. if self.details1.sql_error is not None:
  136. value_and_strategy_desc = value_and_strategy_desc + (
  137. f"\n Error 1: '{self.details1.sql_error}'"
  138. )
  139. if self.details2.sql_error is not None:
  140. value_and_strategy_desc = value_and_strategy_desc + (
  141. f"\n Error 2: '{self.details2.sql_error}'"
  142. )
  143. sql_desc = f"\n Query 1: {self.details1.sql}\n Query 2: {self.details2.sql}"
  144. expression_hash = (
  145. f"\n Expression hash: {self.concerned_expression_hash}"
  146. if self.concerned_expression_hash is not None
  147. else ""
  148. )
  149. return f"{self.error_type}: {self.message}{location_desc}{error_desc}.{expression_desc}{value_and_strategy_desc}{sql_desc}{expression_hash}"