test_builder.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. import datetime
  2. import re
  3. from django.utils import timezone
  4. from snuba_sdk.column import Column
  5. from snuba_sdk.conditions import Condition, Op, Or
  6. from snuba_sdk.function import Function
  7. from snuba_sdk.orderby import Direction, OrderBy
  8. from sentry.exceptions import InvalidSearchQuery
  9. from sentry.search.events.builder import QueryBuilder
  10. from sentry.testutils.cases import TestCase
  11. from sentry.utils.snuba import Dataset, QueryOutsideRetentionError
  12. class QueryBuilderTest(TestCase):
  13. def setUp(self):
  14. self.start = datetime.datetime(2015, 5, 18, 10, 15, 1, tzinfo=timezone.utc)
  15. self.end = datetime.datetime(2015, 5, 19, 10, 15, 1, tzinfo=timezone.utc)
  16. self.projects = [1, 2, 3]
  17. self.params = {
  18. "project_id": self.projects,
  19. "start": self.start,
  20. "end": self.end,
  21. }
  22. # These conditions should always be on a query when self.params is passed
  23. self.default_conditions = [
  24. Condition(Column("timestamp"), Op.GTE, self.start),
  25. Condition(Column("timestamp"), Op.LT, self.end),
  26. Condition(Column("project_id"), Op.IN, self.projects),
  27. ]
  28. def test_simple_query(self):
  29. query = QueryBuilder(
  30. Dataset.Discover,
  31. self.params,
  32. "user.email:foo@example.com release:1.2.1",
  33. ["user.email", "release"],
  34. )
  35. self.assertCountEqual(
  36. query.where,
  37. [
  38. Condition(Column("email"), Op.EQ, "foo@example.com"),
  39. Condition(Column("release"), Op.EQ, "1.2.1"),
  40. *self.default_conditions,
  41. ],
  42. )
  43. self.assertCountEqual(
  44. query.select,
  45. [
  46. Function("toString", [Column("email")], "user.email"),
  47. Column("release"),
  48. ],
  49. )
  50. query.get_snql_query().validate()
  51. def test_simple_orderby(self):
  52. query = QueryBuilder(
  53. Dataset.Discover,
  54. self.params,
  55. selected_columns=["user.email", "release"],
  56. orderby=["user.email"],
  57. )
  58. self.assertCountEqual(query.where, self.default_conditions)
  59. self.assertCountEqual(
  60. query.orderby,
  61. [OrderBy(Function("toString", [Column("email")], "user.email"), Direction.ASC)],
  62. )
  63. query.get_snql_query().validate()
  64. query = QueryBuilder(
  65. Dataset.Discover,
  66. self.params,
  67. selected_columns=["user.email", "release"],
  68. orderby=["-user.email"],
  69. )
  70. self.assertCountEqual(query.where, self.default_conditions)
  71. self.assertCountEqual(
  72. query.orderby,
  73. [OrderBy(Function("toString", [Column("email")], "user.email"), Direction.DESC)],
  74. )
  75. query.get_snql_query().validate()
  76. def test_environment_filter(self):
  77. query = QueryBuilder(
  78. Dataset.Discover,
  79. self.params,
  80. "environment:prod",
  81. ["environment"],
  82. )
  83. self.assertCountEqual(
  84. query.where,
  85. [
  86. Condition(Column("environment"), Op.EQ, "prod"),
  87. *self.default_conditions,
  88. ],
  89. )
  90. query.get_snql_query().validate()
  91. query = QueryBuilder(
  92. Dataset.Discover,
  93. self.params,
  94. "environment:[dev, prod]",
  95. ["environment"],
  96. )
  97. self.assertCountEqual(
  98. query.where,
  99. [
  100. Condition(Column("environment"), Op.IN, ["dev", "prod"]),
  101. *self.default_conditions,
  102. ],
  103. )
  104. query.get_snql_query().validate()
  105. def test_environment_param(self):
  106. self.params["environment"] = ["", "prod"]
  107. query = QueryBuilder(Dataset.Discover, self.params, selected_columns=["environment"])
  108. self.assertCountEqual(
  109. query.where,
  110. [
  111. *self.default_conditions,
  112. Or(
  113. [
  114. Condition(Column("environment"), Op.IS_NULL),
  115. Condition(Column("environment"), Op.EQ, "prod"),
  116. ]
  117. ),
  118. ],
  119. )
  120. query.get_snql_query().validate()
  121. self.params["environment"] = ["dev", "prod"]
  122. query = QueryBuilder(Dataset.Discover, self.params, selected_columns=["environment"])
  123. self.assertCountEqual(
  124. query.where,
  125. [
  126. *self.default_conditions,
  127. Condition(Column("environment"), Op.IN, ["dev", "prod"]),
  128. ],
  129. )
  130. query.get_snql_query().validate()
  131. def test_project_in_condition_filters(self):
  132. # TODO(snql-boolean): Update this to match the corresponding test in test_filter
  133. project1 = self.create_project()
  134. project2 = self.create_project()
  135. self.params["project_id"] = [project1.id, project2.id]
  136. query = QueryBuilder(
  137. Dataset.Discover,
  138. self.params,
  139. f"project:{project1.slug}",
  140. selected_columns=["environment"],
  141. )
  142. self.assertCountEqual(
  143. query.where,
  144. [
  145. # generated by the search query on project
  146. Condition(Column("project_id"), Op.EQ, project1.id),
  147. Condition(Column("timestamp"), Op.GTE, self.start),
  148. Condition(Column("timestamp"), Op.LT, self.end),
  149. # default project filter from the params
  150. Condition(Column("project_id"), Op.IN, [project1.id, project2.id]),
  151. ],
  152. )
  153. def test_project_in_condition_filters_not_in_project_filter(self):
  154. # TODO(snql-boolean): Update this to match the corresponding test in test_filter
  155. project1 = self.create_project()
  156. project2 = self.create_project()
  157. # params is assumed to be validated at this point, so this query should be invalid
  158. self.params["project_id"] = [project2.id]
  159. with self.assertRaisesRegexp(
  160. InvalidSearchQuery,
  161. re.escape(
  162. f"Invalid query. Project(s) {str(project1.slug)} do not exist or are not actively selected."
  163. ),
  164. ):
  165. QueryBuilder(
  166. Dataset.Discover,
  167. self.params,
  168. f"project:{project1.slug}",
  169. selected_columns=["environment"],
  170. )
  171. def test_project_alias_column(self):
  172. # TODO(snql-boolean): Update this to match the corresponding test in test_filter
  173. project1 = self.create_project()
  174. project2 = self.create_project()
  175. self.params["project_id"] = [project1.id, project2.id]
  176. query = QueryBuilder(Dataset.Discover, self.params, selected_columns=["project"])
  177. self.assertCountEqual(
  178. query.where,
  179. [
  180. Condition(Column("project_id"), Op.IN, [project1.id, project2.id]),
  181. Condition(Column("timestamp"), Op.GTE, self.start),
  182. Condition(Column("timestamp"), Op.LT, self.end),
  183. ],
  184. )
  185. self.assertCountEqual(
  186. query.select,
  187. [
  188. Function(
  189. "transform",
  190. [
  191. Column("project_id"),
  192. [project1.id, project2.id],
  193. [project1.slug, project2.slug],
  194. "",
  195. ],
  196. "project",
  197. )
  198. ],
  199. )
  200. def test_project_alias_column_with_project_condition(self):
  201. project1 = self.create_project()
  202. project2 = self.create_project()
  203. self.params["project_id"] = [project1.id, project2.id]
  204. query = QueryBuilder(
  205. Dataset.Discover, self.params, f"project:{project1.slug}", selected_columns=["project"]
  206. )
  207. self.assertCountEqual(
  208. query.where,
  209. [
  210. # generated by the search query on project
  211. Condition(Column("project_id"), Op.EQ, project1.id),
  212. Condition(Column("timestamp"), Op.GTE, self.start),
  213. Condition(Column("timestamp"), Op.LT, self.end),
  214. # default project filter from the params
  215. Condition(Column("project_id"), Op.IN, [project1.id, project2.id]),
  216. ],
  217. )
  218. # Because of the condition on project there should only be 1 project in the transform
  219. self.assertCountEqual(
  220. query.select,
  221. [
  222. Function(
  223. "transform",
  224. [
  225. Column("project_id"),
  226. [project1.id],
  227. [project1.slug],
  228. "",
  229. ],
  230. "project",
  231. )
  232. ],
  233. )
  234. def test_count_if(self):
  235. query = QueryBuilder(
  236. Dataset.Discover,
  237. self.params,
  238. "",
  239. selected_columns=[
  240. "count_if(event.type,equals,transaction)",
  241. 'count_if(event.type,notEquals,"transaction")',
  242. ],
  243. )
  244. self.assertCountEqual(query.where, self.default_conditions)
  245. self.assertCountEqual(
  246. query.aggregates,
  247. [
  248. Function(
  249. "countIf",
  250. [
  251. Function("equals", [Column("type"), "transaction"]),
  252. ],
  253. "count_if_event_type_equals_transaction",
  254. ),
  255. Function(
  256. "countIf",
  257. [
  258. Function("notEquals", [Column("type"), "transaction"]),
  259. ],
  260. "count_if_event_type_notEquals__transaction",
  261. ),
  262. ],
  263. )
  264. def test_count_if_with_tags(self):
  265. query = QueryBuilder(
  266. Dataset.Discover,
  267. self.params,
  268. "",
  269. selected_columns=[
  270. "count_if(foo,equals,bar)",
  271. 'count_if(foo,notEquals,"baz")',
  272. ],
  273. )
  274. self.assertCountEqual(query.where, self.default_conditions)
  275. self.assertCountEqual(
  276. query.aggregates,
  277. [
  278. Function(
  279. "countIf",
  280. [
  281. Function("equals", [Column("tags[foo]"), "bar"]),
  282. ],
  283. "count_if_foo_equals_bar",
  284. ),
  285. Function(
  286. "countIf",
  287. [
  288. Function("notEquals", [Column("tags[foo]"), "baz"]),
  289. ],
  290. "count_if_foo_notEquals__baz",
  291. ),
  292. ],
  293. )
  294. def test_retention(self):
  295. with self.options({"system.event-retention-days": 10}):
  296. with self.assertRaises(QueryOutsideRetentionError):
  297. QueryBuilder(
  298. Dataset.Discover,
  299. self.params,
  300. "",
  301. selected_columns=[],
  302. )