deltacat 2.0.0b7__py3-none-any.whl → 2.0.0b10__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.
Files changed (73) hide show
  1. deltacat/__init__.py +27 -6
  2. deltacat/api.py +478 -123
  3. deltacat/aws/s3u.py +2 -2
  4. deltacat/benchmarking/conftest.py +1 -1
  5. deltacat/catalog/main/impl.py +12 -6
  6. deltacat/catalog/model/catalog.py +65 -47
  7. deltacat/catalog/model/properties.py +1 -3
  8. deltacat/compute/__init__.py +14 -0
  9. deltacat/compute/converter/constants.py +5 -0
  10. deltacat/compute/converter/converter_session.py +78 -36
  11. deltacat/compute/converter/model/convert_input.py +24 -4
  12. deltacat/compute/converter/model/convert_result.py +61 -0
  13. deltacat/compute/converter/model/converter_session_params.py +52 -10
  14. deltacat/compute/converter/pyiceberg/overrides.py +181 -62
  15. deltacat/compute/converter/steps/convert.py +84 -36
  16. deltacat/compute/converter/steps/dedupe.py +25 -4
  17. deltacat/compute/converter/utils/convert_task_options.py +42 -13
  18. deltacat/compute/converter/utils/iceberg_columns.py +5 -0
  19. deltacat/compute/converter/utils/io.py +82 -11
  20. deltacat/compute/converter/utils/s3u.py +13 -4
  21. deltacat/compute/jobs/__init__.py +0 -0
  22. deltacat/compute/jobs/client.py +404 -0
  23. deltacat/constants.py +4 -4
  24. deltacat/daft/daft_scan.py +7 -3
  25. deltacat/daft/translator.py +126 -0
  26. deltacat/examples/basic_logging.py +5 -3
  27. deltacat/examples/hello_world.py +4 -2
  28. deltacat/examples/indexer/__init__.py +0 -0
  29. deltacat/examples/indexer/aws/__init__.py +0 -0
  30. deltacat/examples/indexer/gcp/__init__.py +0 -0
  31. deltacat/examples/indexer/indexer.py +163 -0
  32. deltacat/examples/indexer/job_runner.py +199 -0
  33. deltacat/io/__init__.py +13 -0
  34. deltacat/io/dataset/__init__.py +0 -0
  35. deltacat/io/dataset/deltacat_dataset.py +91 -0
  36. deltacat/io/datasink/__init__.py +0 -0
  37. deltacat/io/datasink/deltacat_datasink.py +207 -0
  38. deltacat/io/datasource/__init__.py +0 -0
  39. deltacat/io/datasource/deltacat_datasource.py +580 -0
  40. deltacat/io/reader/__init__.py +0 -0
  41. deltacat/io/reader/deltacat_read_api.py +172 -0
  42. deltacat/storage/__init__.py +2 -0
  43. deltacat/storage/model/expression/__init__.py +47 -0
  44. deltacat/storage/model/expression/expression.py +656 -0
  45. deltacat/storage/model/expression/visitor.py +248 -0
  46. deltacat/storage/model/metafile.py +74 -42
  47. deltacat/storage/model/scan/push_down.py +32 -5
  48. deltacat/storage/model/types.py +5 -3
  49. deltacat/storage/rivulet/__init__.py +4 -4
  50. deltacat/tests/_io/reader/__init__.py +0 -0
  51. deltacat/tests/_io/reader/test_deltacat_read_api.py +0 -0
  52. deltacat/tests/compute/converter/test_convert_session.py +209 -46
  53. deltacat/tests/local_deltacat_storage/__init__.py +1 -0
  54. deltacat/tests/storage/model/test_expression.py +327 -0
  55. deltacat/tests/storage/rivulet/fs/test_file_location_provider.py +2 -1
  56. deltacat/tests/storage/rivulet/test_dataset.py +1 -1
  57. deltacat/tests/storage/rivulet/test_manifest.py +1 -1
  58. deltacat/tests/storage/rivulet/writer/test_memtable_dataset_writer.py +1 -1
  59. deltacat/tests/test_deltacat_api.py +50 -9
  60. deltacat/types/media.py +141 -43
  61. deltacat/types/tables.py +35 -7
  62. deltacat/utils/daft.py +2 -2
  63. deltacat/utils/filesystem.py +39 -9
  64. deltacat/utils/polars.py +128 -0
  65. deltacat/utils/pyarrow.py +151 -15
  66. deltacat/utils/ray_utils/concurrency.py +1 -1
  67. deltacat/utils/ray_utils/runtime.py +56 -4
  68. deltacat/utils/url.py +1284 -0
  69. {deltacat-2.0.0b7.dist-info → deltacat-2.0.0b10.dist-info}/METADATA +9 -6
  70. {deltacat-2.0.0b7.dist-info → deltacat-2.0.0b10.dist-info}/RECORD +73 -48
  71. {deltacat-2.0.0b7.dist-info → deltacat-2.0.0b10.dist-info}/LICENSE +0 -0
  72. {deltacat-2.0.0b7.dist-info → deltacat-2.0.0b10.dist-info}/WHEEL +0 -0
  73. {deltacat-2.0.0b7.dist-info → deltacat-2.0.0b10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,656 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from dataclasses import dataclass, field
5
+ from typing import List, TypeVar, Any
6
+
7
+ import pyarrow as pa
8
+
9
+ from deltacat.storage.model.schema import FieldName
10
+
11
+
12
+ R = TypeVar("R")
13
+ C = TypeVar("C")
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Expression(ABC):
18
+ """
19
+ This abstract class serves as the foundation for all expression types
20
+ in the Deltacat expression tree. It defines common functionality and structure
21
+ for expressions.
22
+ """
23
+
24
+ def __str__(self) -> str:
25
+ """Convert the expression to a string representation.
26
+
27
+ Returns:
28
+ str: A string representation of the expression.
29
+ """
30
+ from deltacat.storage.model.expression.visitor import DisplayVisitor
31
+
32
+ return DisplayVisitor().visit(self, None)
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class UnaryExpression(Expression, ABC):
37
+ """
38
+ This abstract class represents operations that work on a single input
39
+ expression (e.g., NOT, IS NULL).
40
+
41
+ Args:
42
+ operand: The expression this unary operation applies to.
43
+ """
44
+
45
+ operand: Expression
46
+
47
+ @classmethod
48
+ def of(cls, operand: Expression | Any) -> UnaryExpression:
49
+ """
50
+ Create a new instance with the given operand.
51
+
52
+ Args:
53
+ operand: The expression or value to use as the operand.
54
+ If not an Expression, it will be converted to a Literal.
55
+
56
+ Returns:
57
+ UnaryExpression: A new instance of this unary expression.
58
+ """
59
+ operand_expr = (
60
+ operand if isinstance(operand, Expression) else Literal(pa.scalar(operand))
61
+ )
62
+ return cls(operand_expr)
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class BinaryExpression(Expression, ABC):
67
+ """
68
+ This abstract class represents operations that work on two input
69
+ expressions (e.g., AND, OR, comparison operators).
70
+
71
+ Args:
72
+ left: The left operand expression.
73
+ right: The right operand expression.
74
+ """
75
+
76
+ left: Expression
77
+ right: Expression
78
+
79
+ def with_left(self, left: Expression) -> BinaryExpression:
80
+ """
81
+ Create a new expression with a different left operand.
82
+
83
+ Args:
84
+ left: The new left operand expression.
85
+
86
+ Returns:
87
+ BinaryExpression: A new binary expression with the updated left operand.
88
+ """
89
+ return type(self)(left, self.right)
90
+
91
+ def with_right(self, right: Expression) -> BinaryExpression:
92
+ """
93
+ Create a new expression with a different right operand.
94
+
95
+ Args:
96
+ right: The new right operand expression.
97
+
98
+ Returns:
99
+ BinaryExpression: A new binary expression with the updated right operand.
100
+ """
101
+ return type(self)(self.left, right)
102
+
103
+ @classmethod
104
+ def of(cls, left: Expression | Any, right: Expression | Any) -> BinaryExpression:
105
+ """
106
+ Create a new instance with the given operands.
107
+
108
+ Args:
109
+ left: The left operand expression or value.
110
+ If not an Expression, it will be converted to a Literal.
111
+ right: The right operand expression or value.
112
+ If not an Expression, it will be converted to a Literal.
113
+
114
+ Returns:
115
+ BinaryExpression: A new instance of this binary expression.
116
+ """
117
+ left_expr = left if isinstance(left, Expression) else Literal(pa.scalar(left))
118
+ right_expr = (
119
+ right if isinstance(right, Expression) else Literal(pa.scalar(right))
120
+ )
121
+ return cls(left_expr, right_expr)
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class BooleanExpression(Expression, ABC):
126
+ """
127
+ This abstract class represents expressions that produce a boolean
128
+ value when evaluated, such as comparisons, logical operations, etc.
129
+ """
130
+
131
+ def and_(self, other: BooleanExpression | Any) -> BooleanExpression:
132
+ """
133
+ Combine this expression with another using logical AND.
134
+
135
+ Args:
136
+ other: The expression to AND with this one.
137
+ If not a BooleanExpression, it will be converted to a Literal.
138
+
139
+ Returns:
140
+ BooleanExpression: A new AND expression.
141
+ """
142
+ other_expr = (
143
+ other if isinstance(other, BooleanExpression) else Literal(pa.scalar(other))
144
+ )
145
+ return And(self, other_expr)
146
+
147
+ def or_(self, other: BooleanExpression | Any) -> BooleanExpression:
148
+ """
149
+ Combine this expression with another using logical OR.
150
+
151
+ Args:
152
+ other: The expression to OR with this one.
153
+ If not a BooleanExpression, it will be converted to a Literal.
154
+
155
+ Returns:
156
+ BooleanExpression: A new OR expression.
157
+ """
158
+ other_expr = (
159
+ other if isinstance(other, BooleanExpression) else Literal(pa.scalar(other))
160
+ )
161
+ return Or(self, other_expr)
162
+
163
+ def not_(self) -> BooleanExpression:
164
+ """
165
+ Negate this expression with logical NOT.
166
+
167
+ Returns:
168
+ BooleanExpression: A new NOT expression.
169
+ """
170
+ return Not(self)
171
+
172
+
173
+ @dataclass(frozen=True)
174
+ class Reference(Expression):
175
+ """
176
+ This class represents a reference to a column or field in a dataset.
177
+
178
+ TODO: Add validation and deltacat schema binding.
179
+
180
+ Args:
181
+ field: The field name of the referenced field.
182
+ index: Optional index value for the field in the schema.
183
+ """
184
+
185
+ field: FieldName
186
+ index: int | None = None
187
+
188
+ @classmethod
189
+ def of(cls, field: FieldName) -> Reference:
190
+ """
191
+ Create a new reference.
192
+
193
+ Args:
194
+ field: The field name of a Deltacat field.
195
+
196
+ Returns:
197
+ Reference: A new reference expression.
198
+ """
199
+ return cls(field)
200
+
201
+ def eq(self, other: Expression | Any) -> Equal:
202
+ """
203
+ Create an equality comparison with this reference.
204
+
205
+ Args:
206
+ other: The expression or value to compare with.
207
+ If not an Expression, it will be converted to a Literal.
208
+
209
+ Returns:
210
+ Equal: A new equality comparison expression.
211
+ """
212
+ if not isinstance(other, Expression):
213
+ other = Literal(pa.scalar(other))
214
+ return Equal(self, other)
215
+
216
+ def ne(self, other: Expression | Any) -> NotEqual:
217
+ """
218
+ Create an inequality comparison with this reference.
219
+
220
+ Args:
221
+ other: The expression or value to compare with.
222
+ If not an Expression, it will be converted to a Literal.
223
+
224
+ Returns:
225
+ NotEqual: A new inequality comparison expression.
226
+ """
227
+ if not isinstance(other, Expression):
228
+ other = Literal(pa.scalar(other))
229
+ return NotEqual(self, other)
230
+
231
+ def gt(self, other: Expression | Any) -> GreaterThan:
232
+ """
233
+ Create a greater than comparison with this reference.
234
+
235
+ Args:
236
+ other: The expression or value to compare with.
237
+ If not an Expression, it will be converted to a Literal.
238
+
239
+ Returns:
240
+ GreaterThan: A new greater than comparison expression.
241
+ """
242
+ if not isinstance(other, Expression):
243
+ other = Literal(pa.scalar(other))
244
+ return GreaterThan(self, other)
245
+
246
+ def lt(self, other: Expression | Any) -> LessThan:
247
+ """
248
+ Create a less than comparison with this reference.
249
+
250
+ Args:
251
+ other: The expression or value to compare with.
252
+ If not an Expression, it will be converted to a Literal.
253
+
254
+ Returns:
255
+ LessThan: A new less than comparison expression.
256
+ """
257
+ if not isinstance(other, Expression):
258
+ other = Literal(pa.scalar(other))
259
+ return LessThan(self, other)
260
+
261
+ def ge(self, other: Expression | Any) -> GreaterThanEqual:
262
+ """
263
+ Create a greater than or equal comparison with this reference.
264
+
265
+ Args:
266
+ other: The expression or value to compare with.
267
+ If not an Expression, it will be converted to a Literal.
268
+
269
+ Returns:
270
+ GreaterThanEqual: A new greater than or equal comparison expression.
271
+ """
272
+ if not isinstance(other, Expression):
273
+ other = Literal(pa.scalar(other))
274
+ return GreaterThanEqual(self, other)
275
+
276
+ def le(self, other: Expression | Any) -> LessThanEqual:
277
+ """
278
+ Create a less than or equal comparison with this reference.
279
+
280
+ Args:
281
+ other: The expression or value to compare with.
282
+ If not an Expression, it will be converted to a Literal.
283
+
284
+ Returns:
285
+ LessThanEqual: A new less than or equal comparison expression.
286
+ """
287
+ if not isinstance(other, Expression):
288
+ other = Literal(pa.scalar(other))
289
+ return LessThanEqual(self, other)
290
+
291
+ def is_null(self) -> IsNull:
292
+ """
293
+ Create an IS NULL check for this reference.
294
+
295
+ Returns:
296
+ IsNull: A new IS NULL expression.
297
+ """
298
+ return IsNull(self)
299
+
300
+ def in_(self, values: List[Expression | Any]) -> In:
301
+ """
302
+ Create an IN check for this reference.
303
+
304
+ Args:
305
+ values: List of expressions or values to check against.
306
+ Non-Expression values will be converted to Literals.
307
+
308
+ Returns:
309
+ In: A new IN expression.
310
+ """
311
+ expr_values = [
312
+ v if isinstance(v, Expression) else Literal(pa.scalar(v)) for v in values
313
+ ]
314
+ return In(self, expr_values)
315
+
316
+ def between(self, lower: Expression | Any, upper: Expression | Any) -> Between:
317
+ """
318
+ Create a BETWEEN check for this reference.
319
+
320
+ Args:
321
+ lower: The lower bound expression or value.
322
+ If not an Expression, it will be converted to a Literal.
323
+ upper: The upper bound expression or value.
324
+ If not an Expression, it will be converted to a Literal.
325
+
326
+ Returns:
327
+ Between: A new BETWEEN expression.
328
+ """
329
+ lower_expr = (
330
+ lower if isinstance(lower, Expression) else Literal(pa.scalar(lower))
331
+ )
332
+ upper_expr = (
333
+ upper if isinstance(upper, Expression) else Literal(pa.scalar(upper))
334
+ )
335
+ return Between(self, lower_expr, upper_expr)
336
+
337
+ def like(self, pattern: str | Expression) -> Like:
338
+ """
339
+ Create a LIKE pattern match for this reference.
340
+
341
+ Args:
342
+ pattern: The pattern to match against.
343
+ If not an Expression, it will be converted to a Literal.
344
+
345
+ Returns:
346
+ Like: A new LIKE expression.
347
+ """
348
+ pattern_expr = (
349
+ pattern if isinstance(pattern, Expression) else Literal(pa.scalar(pattern))
350
+ )
351
+ return Like(self, pattern_expr)
352
+
353
+
354
+ @dataclass(frozen=True)
355
+ class Literal(Expression):
356
+ """
357
+ Literal value using PyArrow scalar.
358
+
359
+ TODO: implement custom deltacat types and literals.
360
+
361
+ Args:
362
+ value: The PyArrow scalar value.
363
+ """
364
+
365
+ value: pa.Scalar
366
+
367
+ @classmethod
368
+ def of(cls, value: Any) -> Literal:
369
+ """
370
+ Create a new literal value.
371
+
372
+ Args:
373
+ value: The value to create a literal for.
374
+ If not a PyArrow Scalar, it will be converted.
375
+
376
+ Returns:
377
+ Literal: A new literal expression.
378
+ """
379
+ if not isinstance(value, pa.Scalar):
380
+ value = pa.scalar(value)
381
+ return cls(value)
382
+
383
+ def eq(self, other: Expression | Any) -> Equal:
384
+ """
385
+ Create an equality comparison with this literal.
386
+
387
+ Args:
388
+ other: The expression or value to compare with.
389
+ If not an Expression, it will be converted to a Literal.
390
+
391
+ Returns:
392
+ Equal: A new equality comparison expression.
393
+ """
394
+ if not isinstance(other, Expression):
395
+ other = Literal(pa.scalar(other))
396
+ return Equal(self, other)
397
+
398
+ def ne(self, other: Expression | Any) -> NotEqual:
399
+ """
400
+ Create an inequality comparison with this literal.
401
+
402
+ Args:
403
+ other: The expression or value to compare with.
404
+ If not an Expression, it will be converted to a Literal.
405
+
406
+ Returns:
407
+ NotEqual: A new inequality comparison expression.
408
+ """
409
+ if not isinstance(other, Expression):
410
+ other = Literal(pa.scalar(other))
411
+ return NotEqual(self, other)
412
+
413
+
414
+ @dataclass(frozen=True)
415
+ class Equal(BinaryExpression, BooleanExpression):
416
+ """
417
+ Equality comparison (=).
418
+
419
+ Args:
420
+ left: The left operand expression.
421
+ right: The right operand expression.
422
+ """
423
+
424
+ pass
425
+
426
+
427
+ @dataclass(frozen=True)
428
+ class NotEqual(BinaryExpression, BooleanExpression):
429
+ """
430
+ Inequality comparison (<>).
431
+
432
+ Args:
433
+ left: The left operand expression.
434
+ right: The right operand expression.
435
+ """
436
+
437
+ pass
438
+
439
+
440
+ @dataclass(frozen=True)
441
+ class GreaterThan(BinaryExpression, BooleanExpression):
442
+ """
443
+ Greater than comparison (>).
444
+
445
+ Args:
446
+ left: The left operand expression.
447
+ right: The right operand expression.
448
+ """
449
+
450
+ pass
451
+
452
+
453
+ @dataclass(frozen=True)
454
+ class LessThan(BinaryExpression, BooleanExpression):
455
+ """
456
+ Less than comparison (<).
457
+
458
+ Args:
459
+ left: The left operand expression.
460
+ right: The right operand expression.
461
+ """
462
+
463
+ pass
464
+
465
+
466
+ @dataclass(frozen=True)
467
+ class GreaterThanEqual(BinaryExpression, BooleanExpression):
468
+ """
469
+ Greater than or equal comparison (>=).
470
+
471
+ Args:
472
+ left: The left operand expression.
473
+ right: The right operand expression.
474
+ """
475
+
476
+ pass
477
+
478
+
479
+ @dataclass(frozen=True)
480
+ class LessThanEqual(BinaryExpression, BooleanExpression):
481
+ """
482
+ Less than or equal comparison (<=).
483
+
484
+ Args:
485
+ left: The left operand expression.
486
+ right: The right operand expression.
487
+ """
488
+
489
+ pass
490
+
491
+
492
+ @dataclass(frozen=True)
493
+ class And(BinaryExpression, BooleanExpression):
494
+ """
495
+ Logical AND operation.
496
+
497
+ Args:
498
+ left: The left operand expression.
499
+ right: The right operand expression.
500
+ """
501
+
502
+ pass
503
+
504
+
505
+ @dataclass(frozen=True)
506
+ class Or(BinaryExpression, BooleanExpression):
507
+ """
508
+ Logical OR operation.
509
+
510
+ Args:
511
+ left: The left operand expression.
512
+ right: The right operand expression.
513
+ """
514
+
515
+ pass
516
+
517
+
518
+ @dataclass(frozen=True)
519
+ class Not(UnaryExpression, BooleanExpression):
520
+ """
521
+ Logical NOT operation.
522
+
523
+ Args:
524
+ operand: The boolean expression to negate.
525
+ """
526
+
527
+ operand: BooleanExpression
528
+
529
+
530
+ @dataclass(frozen=True)
531
+ class In(BooleanExpression):
532
+ """
533
+ Checks if a value is in a list of values.
534
+
535
+ Args:
536
+ value: The expression to check.
537
+ values: List of expressions to check against.
538
+ """
539
+
540
+ value: Expression
541
+ values: List[Expression] = field(default_factory=list)
542
+
543
+ @classmethod
544
+ def of(cls, value: Expression | Any, values: List[Expression | Any]) -> In:
545
+ """
546
+ Create a new IN expression.
547
+
548
+ Args:
549
+ value: The expression or value to check.
550
+ If not an Expression, it will be converted to a Literal.
551
+ values: List of expressions or values to check against.
552
+ Non-Expression values will be converted to Literals.
553
+
554
+ Returns:
555
+ In: A new IN expression.
556
+ """
557
+ value_expr = (
558
+ value if isinstance(value, Expression) else Literal(pa.scalar(value))
559
+ )
560
+ value_exprs = [
561
+ v if isinstance(v, Expression) else Literal(pa.scalar(v)) for v in values
562
+ ]
563
+ return cls(value_expr, value_exprs)
564
+
565
+
566
+ @dataclass(frozen=True)
567
+ class Between(BooleanExpression):
568
+ """
569
+ Checks if a value is between two other values (inclusive).
570
+
571
+ Args:
572
+ value: The expression to check.
573
+ lower: The lower bound expression.
574
+ upper: The upper bound expression.
575
+ """
576
+
577
+ value: Expression
578
+ lower: Expression
579
+ upper: Expression
580
+
581
+ @classmethod
582
+ def of(
583
+ cls, value: Expression | Any, lower: Expression | Any, upper: Expression | Any
584
+ ) -> Between:
585
+ """
586
+ Create a new BETWEEN expression.
587
+
588
+ Args:
589
+ value: The expression or value to check.
590
+ If not an Expression, it will be converted to a Literal.
591
+ lower: The lower bound expression or value.
592
+ If not an Expression, it will be converted to a Literal.
593
+ upper: The upper bound expression or value.
594
+ If not an Expression, it will be converted to a Literal.
595
+
596
+ Returns:
597
+ Between: A new BETWEEN expression.
598
+ """
599
+ value_expr = (
600
+ value if isinstance(value, Expression) else Literal(pa.scalar(value))
601
+ )
602
+ lower_expr = (
603
+ lower if isinstance(lower, Expression) else Literal(pa.scalar(lower))
604
+ )
605
+ upper_expr = (
606
+ upper if isinstance(upper, Expression) else Literal(pa.scalar(upper))
607
+ )
608
+ return cls(value_expr, lower_expr, upper_expr)
609
+
610
+
611
+ @dataclass(frozen=True)
612
+ class Like(BooleanExpression):
613
+ """
614
+ Pattern matching for string values.
615
+
616
+ Args:
617
+ value: The expression to check.
618
+ pattern: The pattern to match against.
619
+ """
620
+
621
+ value: Expression
622
+ pattern: Expression
623
+
624
+ @classmethod
625
+ def of(cls, value: Expression | Any, pattern: Expression | Any) -> Like:
626
+ """
627
+ Create a new LIKE expression.
628
+
629
+ Args:
630
+ value: The expression or value to check.
631
+ If not an Expression, it will be converted to a Literal.
632
+ pattern: The pattern expression or value to match against.
633
+ If not an Expression, it will be converted to a Literal.
634
+
635
+ Returns:
636
+ Like: A new LIKE expression.
637
+ """
638
+ value_expr = (
639
+ value if isinstance(value, Expression) else Literal(pa.scalar(value))
640
+ )
641
+ pattern_expr = (
642
+ pattern if isinstance(pattern, Expression) else Literal(pa.scalar(pattern))
643
+ )
644
+ return cls(value_expr, pattern_expr)
645
+
646
+
647
+ @dataclass(frozen=True)
648
+ class IsNull(UnaryExpression, BooleanExpression):
649
+ """
650
+ Checks if a value is NULL.
651
+
652
+ Args:
653
+ operand: The expression to check for NULL.
654
+ """
655
+
656
+ pass