pixeltable 0.1.2__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pixeltable might be problematic. Click here for more details.

Files changed (140) hide show
  1. pixeltable/__init__.py +21 -4
  2. pixeltable/catalog/__init__.py +13 -0
  3. pixeltable/catalog/catalog.py +159 -0
  4. pixeltable/catalog/column.py +200 -0
  5. pixeltable/catalog/dir.py +32 -0
  6. pixeltable/catalog/globals.py +33 -0
  7. pixeltable/catalog/insertable_table.py +191 -0
  8. pixeltable/catalog/named_function.py +36 -0
  9. pixeltable/catalog/path.py +58 -0
  10. pixeltable/catalog/path_dict.py +139 -0
  11. pixeltable/catalog/schema_object.py +39 -0
  12. pixeltable/catalog/table.py +581 -0
  13. pixeltable/catalog/table_version.py +749 -0
  14. pixeltable/catalog/table_version_path.py +133 -0
  15. pixeltable/catalog/view.py +203 -0
  16. pixeltable/client.py +520 -31
  17. pixeltable/dataframe.py +540 -349
  18. pixeltable/env.py +373 -48
  19. pixeltable/exceptions.py +12 -21
  20. pixeltable/exec/__init__.py +9 -0
  21. pixeltable/exec/aggregation_node.py +78 -0
  22. pixeltable/exec/cache_prefetch_node.py +113 -0
  23. pixeltable/exec/component_iteration_node.py +79 -0
  24. pixeltable/exec/data_row_batch.py +95 -0
  25. pixeltable/exec/exec_context.py +22 -0
  26. pixeltable/exec/exec_node.py +61 -0
  27. pixeltable/exec/expr_eval_node.py +217 -0
  28. pixeltable/exec/in_memory_data_node.py +69 -0
  29. pixeltable/exec/media_validation_node.py +43 -0
  30. pixeltable/exec/sql_scan_node.py +225 -0
  31. pixeltable/exprs/__init__.py +24 -0
  32. pixeltable/exprs/arithmetic_expr.py +102 -0
  33. pixeltable/exprs/array_slice.py +71 -0
  34. pixeltable/exprs/column_property_ref.py +77 -0
  35. pixeltable/exprs/column_ref.py +105 -0
  36. pixeltable/exprs/comparison.py +77 -0
  37. pixeltable/exprs/compound_predicate.py +98 -0
  38. pixeltable/exprs/data_row.py +187 -0
  39. pixeltable/exprs/expr.py +586 -0
  40. pixeltable/exprs/expr_set.py +39 -0
  41. pixeltable/exprs/function_call.py +380 -0
  42. pixeltable/exprs/globals.py +69 -0
  43. pixeltable/exprs/image_member_access.py +115 -0
  44. pixeltable/exprs/image_similarity_predicate.py +58 -0
  45. pixeltable/exprs/inline_array.py +107 -0
  46. pixeltable/exprs/inline_dict.py +101 -0
  47. pixeltable/exprs/is_null.py +38 -0
  48. pixeltable/exprs/json_mapper.py +121 -0
  49. pixeltable/exprs/json_path.py +159 -0
  50. pixeltable/exprs/literal.py +54 -0
  51. pixeltable/exprs/object_ref.py +41 -0
  52. pixeltable/exprs/predicate.py +44 -0
  53. pixeltable/exprs/row_builder.py +355 -0
  54. pixeltable/exprs/rowid_ref.py +94 -0
  55. pixeltable/exprs/type_cast.py +53 -0
  56. pixeltable/exprs/variable.py +45 -0
  57. pixeltable/func/__init__.py +9 -0
  58. pixeltable/func/aggregate_function.py +194 -0
  59. pixeltable/func/batched_function.py +53 -0
  60. pixeltable/func/callable_function.py +69 -0
  61. pixeltable/func/expr_template_function.py +82 -0
  62. pixeltable/func/function.py +110 -0
  63. pixeltable/func/function_registry.py +227 -0
  64. pixeltable/func/globals.py +36 -0
  65. pixeltable/func/nos_function.py +202 -0
  66. pixeltable/func/signature.py +166 -0
  67. pixeltable/func/udf.py +163 -0
  68. pixeltable/functions/__init__.py +52 -103
  69. pixeltable/functions/eval.py +216 -0
  70. pixeltable/functions/fireworks.py +61 -0
  71. pixeltable/functions/huggingface.py +120 -0
  72. pixeltable/functions/image.py +16 -0
  73. pixeltable/functions/openai.py +88 -0
  74. pixeltable/functions/pil/image.py +148 -7
  75. pixeltable/functions/string.py +13 -0
  76. pixeltable/functions/together.py +27 -0
  77. pixeltable/functions/util.py +41 -0
  78. pixeltable/functions/video.py +62 -0
  79. pixeltable/iterators/__init__.py +3 -0
  80. pixeltable/iterators/base.py +48 -0
  81. pixeltable/iterators/document.py +311 -0
  82. pixeltable/iterators/video.py +89 -0
  83. pixeltable/metadata/__init__.py +54 -0
  84. pixeltable/metadata/converters/convert_10.py +18 -0
  85. pixeltable/metadata/schema.py +211 -0
  86. pixeltable/plan.py +656 -0
  87. pixeltable/store.py +413 -182
  88. pixeltable/tests/conftest.py +143 -86
  89. pixeltable/tests/test_audio.py +65 -0
  90. pixeltable/tests/test_catalog.py +27 -0
  91. pixeltable/tests/test_client.py +14 -14
  92. pixeltable/tests/test_component_view.py +372 -0
  93. pixeltable/tests/test_dataframe.py +433 -0
  94. pixeltable/tests/test_dirs.py +78 -62
  95. pixeltable/tests/test_document.py +117 -0
  96. pixeltable/tests/test_exprs.py +591 -135
  97. pixeltable/tests/test_function.py +297 -67
  98. pixeltable/tests/test_functions.py +283 -1
  99. pixeltable/tests/test_migration.py +43 -0
  100. pixeltable/tests/test_nos.py +54 -0
  101. pixeltable/tests/test_snapshot.py +208 -0
  102. pixeltable/tests/test_table.py +1086 -258
  103. pixeltable/tests/test_transactional_directory.py +42 -0
  104. pixeltable/tests/test_types.py +5 -11
  105. pixeltable/tests/test_video.py +149 -34
  106. pixeltable/tests/test_view.py +530 -0
  107. pixeltable/tests/utils.py +186 -45
  108. pixeltable/tool/create_test_db_dump.py +149 -0
  109. pixeltable/type_system.py +490 -133
  110. pixeltable/utils/__init__.py +17 -46
  111. pixeltable/utils/clip.py +12 -15
  112. pixeltable/utils/coco.py +136 -0
  113. pixeltable/utils/documents.py +39 -0
  114. pixeltable/utils/filecache.py +195 -0
  115. pixeltable/utils/help.py +11 -0
  116. pixeltable/utils/media_store.py +76 -0
  117. pixeltable/utils/parquet.py +126 -0
  118. pixeltable/utils/pytorch.py +172 -0
  119. pixeltable/utils/s3.py +13 -0
  120. pixeltable/utils/sql.py +17 -0
  121. pixeltable/utils/transactional_directory.py +35 -0
  122. pixeltable-0.2.1.dist-info/LICENSE +18 -0
  123. pixeltable-0.2.1.dist-info/METADATA +119 -0
  124. pixeltable-0.2.1.dist-info/RECORD +125 -0
  125. {pixeltable-0.1.2.dist-info → pixeltable-0.2.1.dist-info}/WHEEL +1 -1
  126. pixeltable/catalog.py +0 -1421
  127. pixeltable/exprs.py +0 -1745
  128. pixeltable/function.py +0 -269
  129. pixeltable/functions/clip.py +0 -10
  130. pixeltable/functions/pil/__init__.py +0 -23
  131. pixeltable/functions/tf.py +0 -21
  132. pixeltable/index.py +0 -57
  133. pixeltable/tests/test_dict.py +0 -24
  134. pixeltable/tests/test_tf.py +0 -69
  135. pixeltable/tf.py +0 -33
  136. pixeltable/utils/tf.py +0 -33
  137. pixeltable/utils/video.py +0 -32
  138. pixeltable-0.1.2.dist-info/LICENSE +0 -201
  139. pixeltable-0.1.2.dist-info/METADATA +0 -89
  140. pixeltable-0.1.2.dist-info/RECORD +0 -37
@@ -0,0 +1,530 @@
1
+ import logging
2
+
3
+ import PIL
4
+ import pytest
5
+
6
+ import pixeltable as pxt
7
+ from pixeltable import catalog
8
+ from pixeltable import exceptions as excs
9
+ from pixeltable.tests.utils import create_test_tbl, assert_resultset_eq
10
+ from pixeltable.type_system import IntType, FloatType, ImageType
11
+
12
+ logger = logging.getLogger('pixeltable')
13
+
14
+ class TestView:
15
+ """
16
+ TODO:
17
+ - test tree of views
18
+ - test consecutive component views
19
+
20
+ """
21
+ def create_tbl(self, cl: pxt.Client) -> catalog.InsertableTable:
22
+ """Create table with computed columns"""
23
+ t = create_test_tbl(cl)
24
+ t.add_column(d1=t.c3 - 1)
25
+ # add column that can be updated
26
+ t.add_column(c10=FloatType(nullable=True))
27
+ t.update({'c10': t.c3})
28
+ # computed column that depends on two columns: exercise duplicate elimination during query construction
29
+ t.add_column(d2=t.c3 - t.c10)
30
+ return t
31
+
32
+ def test_basic(self, test_client: pxt.Client) -> None:
33
+ cl = test_client
34
+ t = self.create_tbl(cl)
35
+
36
+ # create view with filter and computed columns
37
+ schema = {
38
+ 'v1': t.c3 * 2.0,
39
+ 'v2': t.c6.f5,
40
+ }
41
+ v = cl.create_view('test_view', t, schema=schema, filter=t.c2 < 10)
42
+ # TODO: test repr more thoroughly
43
+ _ = v.__repr__()
44
+ assert_resultset_eq(
45
+ v.select(v.v1).order_by(v.c2).collect(),
46
+ t.select(t.c3 * 2.0).where(t.c2 < 10).order_by(t.c2).collect())
47
+ # view-only query; returns the same result
48
+ assert_resultset_eq(
49
+ v.select(v.v1).order_by(v.v1).collect(),
50
+ t.select(t.c3 * 2.0).where(t.c2 < 10).order_by(t.c2).collect())
51
+ # computed columns that don't reference the base table
52
+ v.add_column(v3=v.v1 * 2.0)
53
+ v.add_column(v4=v.v2[0])
54
+
55
+ def check_view(t: pxt.Table, v: pxt.Table) -> None:
56
+ assert v.count() == t.where(t.c2 < 10).count()
57
+ assert_resultset_eq(
58
+ v.select(v.v1).order_by(v.c2).collect(),
59
+ t.select(t.c3 * 2.0).where(t.c2 < 10).order_by(t.c2).collect())
60
+ assert_resultset_eq(
61
+ v.select(v.v3).order_by(v.c2).collect(),
62
+ t.select(t.c3 * 4.0).where(t.c2 < 10).order_by(t.c2).collect())
63
+ assert_resultset_eq(
64
+ v.select(v.v4).order_by(v.c2).collect(),
65
+ t.select(t.c6.f5[0]).where(t.c2 < 10).order_by(t.c2).collect())
66
+ check_view(t, v)
67
+
68
+ # check view md after reload
69
+ cl = pxt.Client(reload=True)
70
+ t = cl.get_table('test_tbl')
71
+ v = cl.get_table('test_view')
72
+ check_view(t, v)
73
+
74
+ view_query = v.select(v.v1).order_by(v.c2)
75
+ base_query = t.select(t.c3 * 2.0).where(t.c2 < 10).order_by(t.c2)
76
+
77
+ # insert data: of 20 new rows, only 10 are reflected in the view
78
+ rows = list(t.select(t.c1, t.c1n, t.c2, t.c3, t.c4, t.c5, t.c6, t.c7, t.c10).where(t.c2 < 20).collect())
79
+ status = t.insert(rows)
80
+ assert status.num_rows == 30
81
+ assert t.count() == 120
82
+ check_view(t, v)
83
+
84
+ # update data: cascade to view
85
+ status = t.update({'c4': True, 'c3': t.c3 + 1.0, 'c10': t.c10 - 1.0}, where=t.c2 < 5, cascade=True)
86
+ assert status.num_rows == 10 * 2 # *2: rows affected in both base table and view
87
+ assert t.count() == 120
88
+ check_view(t, v)
89
+
90
+ # base table delete is reflected in view
91
+ status = t.delete(where=t.c2 < 5)
92
+ status.num_rows == 10 * 2 # *2: rows affected in both base table and view
93
+ assert t.count() == 110
94
+ check_view(t, v)
95
+
96
+ # test delete view
97
+ cl.drop_table('test_view')
98
+ with pytest.raises(excs.Error) as exc_info:
99
+ _ = cl.get_table('test_view')
100
+ assert 'No such path:' in str(exc_info.value)
101
+ cl = pxt.Client(reload=True)
102
+ # still true after reload
103
+ with pytest.raises(excs.Error) as exc_info:
104
+ _ = cl.get_table('test_view')
105
+ assert 'No such path:' in str(exc_info.value)
106
+
107
+ t = cl.get_table('test_tbl')
108
+ with pytest.raises(excs.Error) as exc_info:
109
+ _ = cl.create_view('lambda_view', t, schema={'v1': lambda c3: c3 * 2.0})
110
+ assert 'computed with a callable' in str(exc_info.value).lower()
111
+
112
+ def test_parallel_views(self, test_client: pxt.Client) -> None:
113
+ """Two views over the same base table, with non-overlapping filters"""
114
+ cl = test_client
115
+ t = self.create_tbl(cl)
116
+
117
+ # create view with filter and computed columns
118
+ v1 = cl.create_view('v1', t, schema={'v1': t.c3 * 2}, filter=t.c2 < 10)
119
+ # create another view with a non-overlapping filter and computed columns
120
+ v2 = cl.create_view('v2', t, schema={'v1': t.c3 * 3}, filter=(t.c2 < 20) & (t.c2 >= 10))
121
+
122
+ # sanity checks
123
+ v1_query = v1.select(v1.v1).order_by(v1.c2)
124
+ v2_query = v2.select(v2.v1).order_by(v2.c2)
125
+ b1_query = t.select(t.c3 * 2).where(t.c2 < 10).order_by(t.c2)
126
+ b2_query = t.select(t.c3 * 3).where((t.c2 >= 10) & (t.c2 < 20)).order_by(t.c2)
127
+ assert_resultset_eq(v1_query.collect(), b1_query.collect())
128
+ assert_resultset_eq(v2_query.collect(), b2_query.collect())
129
+
130
+ # insert data: of 20 new rows, only 10 show up in each view
131
+ rows = list(t.select(t.c1, t.c1n, t.c2, t.c3, t.c4, t.c5, t.c6, t.c7, t.c10).where(t.c2 < 20).collect())
132
+ status = t.insert(rows)
133
+ assert status.num_rows == 40
134
+ assert t.count() == 120
135
+ assert v1.count() == 20
136
+ assert v2.count() == 20
137
+ assert_resultset_eq(v1_query.collect(), b1_query.collect())
138
+ assert_resultset_eq(v2_query.collect(), b2_query.collect())
139
+
140
+ # update data: cascade to views
141
+ status = t.update(
142
+ {'c4': True, 'c3': t.c3 + 1, 'c10': t.c10 - 1.0}, where=(t.c2 >= 5) & (t.c2 < 15), cascade=True)
143
+ assert status.num_rows == 20 * 2 # *2: rows affected in both base table and view
144
+ assert t.count() == 120
145
+ assert v1.count() == 20
146
+ assert v2.count() == 20
147
+ assert_resultset_eq(v1_query.collect(), b1_query.collect())
148
+ assert_resultset_eq(v2_query.collect(), b2_query.collect())
149
+
150
+
151
+ # base table delete is reflected in view
152
+ status = t.delete(where=(t.c2 >= 5) & (t.c2 < 15))
153
+ status.num_rows == 10 * 2 # *2: rows affected in both base table and view
154
+ assert t.count() == 100
155
+ assert v1.count() == 10
156
+ assert v2.count() == 10
157
+ assert_resultset_eq(v1_query.collect(), b1_query.collect())
158
+ assert_resultset_eq(v2_query.collect(), b2_query.collect())
159
+
160
+ def test_chained_views(self, test_client: pxt.Client) -> None:
161
+ """Two views, the second one is a view over the first one"""
162
+ cl = test_client
163
+ t = self.create_tbl(cl)
164
+
165
+ # create view with filter and computed columns
166
+ v1 = cl.create_view('v1', t, schema={'col1': t.c3 * 2}, filter=t.c2 < 10)
167
+ # create a view on top of v1
168
+ v2_schema = {
169
+ 'col2': t.c3 * 3, # only base
170
+ 'col3': v1.col1 / 2, # only v1
171
+ 'col4': t.c10 + v1.col1, # both base and v1
172
+ }
173
+ v2 = cl.create_view('v2', v1, schema=v2_schema, filter=t.c2 < 5)
174
+
175
+ def check_views():
176
+ assert_resultset_eq(
177
+ v1.select(v1.col1).order_by(v1.c2).collect(),
178
+ t.select(t.c3 * 2).where(t.c2 < 10).order_by(t.c2).collect())
179
+ assert_resultset_eq(
180
+ v2.select(v2.col1).order_by(v2.c2).collect(),
181
+ v1.select(v1.col1).where(v1.c2 < 5).order_by(v1.c2).collect())
182
+ assert_resultset_eq(
183
+ v2.select(v2.col2).order_by(v2.c2).collect(),
184
+ t.select(t.c3 * 3).where(t.c2 < 5).order_by(t.c2).collect())
185
+ assert_resultset_eq(
186
+ v2.select(v2.col3).order_by(v2.c2).collect(),
187
+ v1.select(v1.col1 / 2).where(v1.c2 < 5).order_by(v2.c2).collect())
188
+ assert_resultset_eq(
189
+ v2.select(v2.col4).order_by(v2.c2).collect(),
190
+ v1.select(v1.c10 + v1.col1).where(v1.c2 < 5).order_by(v1.c2).collect())
191
+ #t.select(t.c10 * 2).where(t.c2 < 5).order_by(t.c2).collect())
192
+ check_views()
193
+
194
+ # insert data: of 20 new rows; 10 show up in v1, 5 in v2
195
+ base_version, v1_version, v2_version = t.version(), v1.version(), v2.version()
196
+ rows = list(t.select(t.c1, t.c1n, t.c2, t.c3, t.c4, t.c5, t.c6, t.c7, t.c10).where(t.c2 < 20).collect())
197
+ status = t.insert(rows)
198
+ assert status.num_rows == 20 + 10 + 5
199
+ assert t.count() == 120
200
+ assert v1.count() == 20
201
+ assert v2.count() == 10
202
+ # all versions were incremented
203
+ assert t.version() == base_version + 1
204
+ assert v1.version() == v1_version + 1
205
+ assert v2.version() == v2_version + 1
206
+ check_views()
207
+
208
+ # update data: cascade to both views
209
+ base_version, v1_version, v2_version = t.version(), v1.version(), v2.version()
210
+ status = t.update({'c4': True, 'c3': t.c3 + 1}, where=t.c2 < 15, cascade=True)
211
+ assert status.num_rows == 30 + 20 + 10
212
+ assert t.count() == 120
213
+ # all versions were incremented
214
+ assert t.version() == base_version + 1
215
+ assert v1.version() == v1_version + 1
216
+ assert v2.version() == v2_version + 1
217
+ check_views()
218
+
219
+ # update data: cascade only to v2
220
+ base_version, v1_version, v2_version = t.version(), v1.version(), v2.version()
221
+ status = t.update({'c10': t.c10 - 1.0}, where=t.c2 < 15, cascade=True)
222
+ assert status.num_rows == 30 + 10
223
+ assert t.count() == 120
224
+ # v1 did not get updated
225
+ assert t.version() == base_version + 1
226
+ assert v1.version() == v1_version
227
+ assert v2.version() == v2_version + 1
228
+ check_views()
229
+
230
+ # base table delete is reflected in both views
231
+ base_version, v1_version, v2_version = t.version(), v1.version(), v2.version()
232
+ status = t.delete(where=t.c2 == 0)
233
+ status.num_rows == 1 + 1 + 1
234
+ assert t.count() == 118
235
+ assert v1.count() == 18
236
+ assert v2.count() == 8
237
+ # all versions were incremented
238
+ assert t.version() == base_version + 1
239
+ assert v1.version() == v1_version + 1
240
+ assert v2.version() == v2_version + 1
241
+ check_views()
242
+
243
+ # base table delete is reflected only in v1
244
+ base_version, v1_version, v2_version = t.version(), v1.version(), v2.version()
245
+ status = t.delete(where=t.c2 == 5)
246
+ status.num_rows == 1 + 1
247
+ assert t.count() == 116
248
+ assert v1.count() == 16
249
+ assert v2.count() == 8
250
+ # v2 was not updated
251
+ assert t.version() == base_version + 1
252
+ assert v1.version() == v1_version + 1
253
+ assert v2.version() == v2_version
254
+ check_views()
255
+
256
+ def test_unstored_columns(self, test_client: pxt.Client) -> None:
257
+ """Test chained views with unstored columns"""
258
+ # create table with image column and two updateable int columns
259
+ cl = test_client
260
+ schema = {
261
+ 'img': ImageType(),
262
+ 'int1': IntType(nullable=False),
263
+ 'int2': IntType(nullable=False),
264
+ }
265
+ t = cl.create_table('test_tbl', schema)
266
+ # populate table with images of a defined size
267
+ width, height = 100, 100
268
+ rows = [
269
+ {
270
+ 'img': PIL.Image.new('RGB', (width, height), color=(0, 0, 0)).tobytes('jpeg', 'RGB'),
271
+ 'int1': i,
272
+ 'int2': i,
273
+ }
274
+ for i in range(100)
275
+ ]
276
+ t.insert(rows)
277
+
278
+ # view with unstored column that depends on int1 and a manually updated column (int4)
279
+ v1_schema = {
280
+ 'img2': {
281
+ 'value': t.img.crop([t.int1, t.int1, width, height]),
282
+ 'stored': False,
283
+ },
284
+ 'int3': t.int1 * 2,
285
+ 'int4': IntType(nullable=True), # TODO: add default
286
+ }
287
+ logger.debug('******************* CREATE V1')
288
+ v1 = cl.create_view('v1', t, schema=v1_schema)
289
+ v1.update({'int4': 1})
290
+ _ = v1.select(v1.img2.width, v1.img2.height).collect()
291
+
292
+ # view with stored column that depends on t and view1
293
+ v2_schema = {
294
+ 'img3': {
295
+ # use the actual width and height of the image (not 100, which will pad the image)
296
+ 'value': v1.img2.crop([t.int1 + t.int2, v1.int3 + v1.int4, v1.img2.width, v1.img2.height]),
297
+ 'stored': True,
298
+ },
299
+ }
300
+ logger.debug('******************* CREATE V2')
301
+ v2 = cl.create_view('v2', v1, schema=v2_schema, filter=v1.int1 < 10)
302
+
303
+ def check_views() -> None:
304
+ assert_resultset_eq(
305
+ v1.select(v1.img2.width, v1.img2.height).order_by(v1.int1).collect(),
306
+ t.select(t.img.width - t.int1, t.img.height - t.int1).order_by(t.int1).collect())
307
+ assert_resultset_eq(
308
+ v2.select(v2.img3.width, v2.img3.height).order_by(v2.int1).collect(),
309
+ v1.select(v1.img2.width - v1.int1 - v1.int2, v1.img2.height - v1.int3 - v1.int4)\
310
+ .where(v1.int1 < 10).order_by(v1.int1).collect())
311
+ check_views()
312
+
313
+ logger.debug('******************* INSERT')
314
+ status = t.insert(rows, fail_on_exception=False)
315
+ v1.update({'int4': 1}, where=v1.int4 == None)
316
+ logger.debug('******************* POST INSERT')
317
+ check_views()
318
+
319
+ # update int1:
320
+ # - cascades to v1 and v2
321
+ # - removes a row from v2 (only 9 rows in t now qualify)
322
+ logger.debug('******************* UPDATE INT1')
323
+ t.update({'int1': t.int1 + 1})
324
+ logger.debug('******************* POST UPDATE INT1')
325
+ check_views()
326
+
327
+ # update int2:
328
+ # - cascades only to v2
329
+ # - but requires join against v1 to access int4
330
+ # TODO: but requires join against v1 to access int3 and int4
331
+ logger.debug('******************* UPDATE INT2')
332
+ t.update({'int2': t.int2 + 1})
333
+ logger.debug('******************* POST UPDATE INT2')
334
+ check_views()
335
+
336
+ def test_computed_cols(self, test_client: pxt.Client) -> None:
337
+ cl = test_client
338
+ t = self.create_tbl(cl)
339
+
340
+ # create view with computed columns
341
+ schema = {
342
+ 'v1': t.c3 * 2.0,
343
+ 'v2': t.c6.f5,
344
+ }
345
+ v = cl.create_view('test_view', t, schema=schema)
346
+ assert_resultset_eq(
347
+ v.select(v.v1).order_by(v.c2).show(0),
348
+ t.select(t.c3 * 2.0).order_by(t.c2).show(0))
349
+ # computed columns that don't reference the base table
350
+ v.add_column(v3=v.v1 * 2.0)
351
+ v.add_column(v4=v.v2[0])
352
+
353
+ # use view md after reload
354
+ cl = pxt.Client(reload=True)
355
+ t = cl.get_table('test_tbl')
356
+ v = cl.get_table('test_view')
357
+
358
+ # insert data
359
+ rows = list(t.select(t.c1, t.c1n, t.c2, t.c3, t.c4, t.c5, t.c6, t.c7, t.c10).collect())
360
+ t.insert(rows)
361
+ assert t.count() == 200
362
+ assert_resultset_eq(
363
+ v.select(v.v1).order_by(v.c2).show(0),
364
+ t.select(t.c3 * 2.0).order_by(t.c2).show(0))
365
+
366
+ # update data: cascade to view
367
+ t.update({'c4': True, 'c3': t.c3 + 1.0, 'c10': t.c10 - 1.0}, where=t.c2 < 5, cascade=True)
368
+ assert t.count() == 200
369
+ assert_resultset_eq(
370
+ v.select(v.v1).order_by(v.c2).show(0),
371
+ t.select(t.c3 * 2.0).order_by(t.c2).show(0))
372
+
373
+ # base table delete is reflected in view
374
+ t.delete(where=t.c2 < 5)
375
+ assert t.count() == 190
376
+ assert_resultset_eq(
377
+ v.select(v.v1).order_by(v.c2).show(0),
378
+ t.select(t.c3 * 2.0).order_by(t.c2).show(0))
379
+
380
+ def test_filter(self, test_client: pxt.Client) -> None:
381
+ cl = test_client
382
+ t = create_test_tbl(cl)
383
+
384
+ # create view with filter
385
+ v = cl.create_view('test_view', t, filter=t.c2 < 10)
386
+ assert_resultset_eq(
387
+ v.order_by(v.c2).show(0),
388
+ t.where(t.c2 < 10).order_by(t.c2).show(0))
389
+
390
+ # use view md after reload
391
+ cl = pxt.Client(reload=True)
392
+ t = cl.get_table('test_tbl')
393
+ v = cl.get_table('test_view')
394
+
395
+ # insert data: of 20 new rows, only 10 are reflected in the view
396
+ rows = list(t.select(t.c1, t.c1n, t.c2, t.c3, t.c4, t.c5, t.c6, t.c7).where(t.c2 < 20).collect())
397
+ t.insert(rows)
398
+ assert t.count() == 120
399
+ assert_resultset_eq(
400
+ v.order_by(v.c2).show(0),
401
+ t.where(t.c2 < 10).order_by(t.c2).show(0))
402
+
403
+ # update data
404
+ t.update({'c4': True, 'c3': t.c3 + 1.0}, where=t.c2 < 5, cascade=True)
405
+ assert t.count() == 120
406
+ assert_resultset_eq(
407
+ v.order_by(v.c2).show(0),
408
+ t.where(t.c2 < 10).order_by(t.c2).show(0))
409
+
410
+ # base table delete is reflected in view
411
+ t.delete(where=t.c2 < 5)
412
+ assert t.count() == 110
413
+ assert_resultset_eq(
414
+ v.order_by(v.c2).show(0),
415
+ t.where(t.c2 < 10).order_by(t.c2).show(0))
416
+
417
+ def test_view_of_snapshot(self, test_client: pxt.Client) -> None:
418
+ """Test view over a snapshot"""
419
+ cl = test_client
420
+ t = self.create_tbl(cl)
421
+ snap = cl.create_view('test_snap', t, is_snapshot=True)
422
+
423
+ # create view with filter and computed columns
424
+ schema = {
425
+ 'v1': snap.c3 * 2.0,
426
+ 'v2': snap.c6.f5,
427
+ }
428
+ v = cl.create_view('test_view', snap, schema=schema, filter=snap.c2 < 10)
429
+
430
+ def check_view(s: pxt.Table, v: pxt.Table) -> None:
431
+ assert v.count() == s.where(s.c2 < 10).count()
432
+ assert_resultset_eq(
433
+ v.select(v.v1).order_by(v.c2).collect(),
434
+ s.select(s.c3 * 2.0).where(s.c2 < 10).order_by(s.c2).collect())
435
+ assert_resultset_eq(
436
+ v.select(v.v2).order_by(v.c2).collect(),
437
+ s.select(s.c6.f5).where(s.c2 < 10).order_by(s.c2).collect())
438
+
439
+ check_view(snap, v)
440
+ # computed columns that don't reference the base table
441
+ v.add_column(v3=v.v1 * 2.0)
442
+ v.add_column(v4=v.v2[0])
443
+ assert v.count() == t.where(t.c2 < 10).count()
444
+
445
+ # use view md after reload
446
+ cl = pxt.Client(reload=True)
447
+ t = cl.get_table('test_tbl')
448
+ snap = cl.get_table('test_snap')
449
+ v = cl.get_table('test_view')
450
+
451
+ # insert data: no changes to view
452
+ rows = list(t.select(t.c1, t.c1n, t.c2, t.c3, t.c4, t.c5, t.c6, t.c7, t.c10).where(t.c2 < 20).collect())
453
+ t.insert(rows)
454
+ assert t.count() == 120
455
+ check_view(snap, v)
456
+
457
+ # update data: no changes to view
458
+ t.update({'c4': True, 'c3': t.c3 + 1.0, 'c10': t.c10 - 1.0}, where=t.c2 < 5, cascade=True)
459
+ assert t.count() == 120
460
+ check_view(snap, v)
461
+
462
+ # base table delete: no change to view
463
+ t.delete(where=t.c2 < 5)
464
+ assert t.count() == 110
465
+ check_view(snap, v)
466
+
467
+ def test_snapshots(self, test_client: pxt.Client) -> None:
468
+ """Test snapshot of a view of a snapshot"""
469
+ cl = test_client
470
+ t = self.create_tbl(cl)
471
+ s = cl.create_view('test_snap', t, is_snapshot=True)
472
+ assert s.select(s.c2).order_by(s.c2).collect()['c2'] == t.select(t.c2).order_by(t.c2).collect()['c2']
473
+
474
+ with pytest.raises(excs.Error) as exc_info:
475
+ v = cl.create_view('test_view', s, schema={'v1': t.c3 * 2.0})
476
+ assert 'value expression cannot be computed in the context of the base test_snap' in str(exc_info.value)
477
+
478
+ with pytest.raises(excs.Error) as exc_info:
479
+ v = cl.create_view('test_view', s, filter=t.c2 < 10)
480
+ assert 'filter cannot be computed in the context of the base test_snap' in str(exc_info.value).lower()
481
+
482
+ # create view with filter and computed columns
483
+ schema = {
484
+ 'v1': s.c3 * 2.0,
485
+ 'v2': s.c6.f5,
486
+ }
487
+ v = cl.create_view('test_view', s, schema=schema, filter=s.c2 < 10)
488
+ orig_view_cols = v.column_names()
489
+ view_s = cl.create_view('test_view_snap', v, is_snapshot=True)
490
+ assert set(view_s.column_names()) == set(orig_view_cols)
491
+
492
+ def check(s1: pxt.Table, v: pxt.Table, s2: pxt.Table) -> None:
493
+ assert s1.where(s1.c2 < 10).count() == v.count()
494
+ assert v.count() == s2.count()
495
+ assert_resultset_eq(
496
+ s1.select(s1.c3 * 2.0, s1.c6.f5).where(s1.c2 < 10).order_by(s1.c2).collect(),
497
+ v.select(v.v1, v.v2).order_by(v.c2).collect())
498
+ assert_resultset_eq(
499
+ v.select(v.c3, v.c6, v.v1, v.v2).order_by(v.c2).collect(),
500
+ s2.select(s2.c3, s2.c6, s2.v1, s2.v2).order_by(s2.c2).collect())
501
+ check(s, v, view_s)
502
+
503
+ # add more columns
504
+ v.add_column(v3=v.v1 * 2.0)
505
+ v.add_column(v4=v.v2[0])
506
+ check(s, v, view_s)
507
+ assert set(view_s.column_names()) == set(orig_view_cols)
508
+
509
+ # check md after reload
510
+ cl = pxt.Client(reload=True)
511
+ t = cl.get_table('test_tbl')
512
+ view_s = cl.get_table('test_view_snap')
513
+ check(s, v, view_s)
514
+ assert set(view_s.column_names()) == set(orig_view_cols)
515
+
516
+ # insert data: no changes to snapshot
517
+ rows = list(t.select(t.c1, t.c1n, t.c2, t.c3, t.c4, t.c5, t.c6, t.c7, t.c10).where(t.c2 < 20).collect())
518
+ t.insert(rows)
519
+ assert t.count() == 120
520
+ check(s, v, view_s)
521
+
522
+ # update data: no changes to snapshot
523
+ t.update({'c4': True, 'c3': t.c3 + 1.0, 'c10': t.c10 - 1.0}, where=t.c2 < 5, cascade=True)
524
+ assert t.count() == 120
525
+ check(s, v, view_s)
526
+
527
+ # base table delete: no changes to snapshot
528
+ t.delete(where=t.c2 < 5)
529
+ assert t.count() == 110
530
+ check(s, v, view_s)