omlish 0.0.0.dev229__py3-none-any.whl → 0.0.0.dev230__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. omlish/__about__.py +2 -2
  2. omlish/collections/__init__.py +15 -15
  3. omlish/collections/frozen.py +0 -2
  4. omlish/collections/identity.py +0 -3
  5. omlish/collections/indexed.py +0 -2
  6. omlish/collections/mappings.py +0 -3
  7. omlish/collections/ordered.py +0 -2
  8. omlish/collections/persistent/__init__.py +0 -0
  9. omlish/collections/sorted/__init__.py +0 -0
  10. omlish/collections/{skiplist.py → sorted/skiplist.py} +0 -1
  11. omlish/collections/{sorted.py → sorted/sorted.py} +2 -5
  12. omlish/collections/unmodifiable.py +0 -2
  13. omlish/dispatch/dispatch.py +4 -1
  14. omlish/dispatch/methods.py +4 -0
  15. omlish/formats/json/__init__.py +5 -0
  16. omlish/io/compress/brotli.py +3 -3
  17. omlish/io/fdio/__init__.py +3 -0
  18. omlish/marshal/__init__.py +10 -10
  19. omlish/marshal/base.py +1 -1
  20. omlish/marshal/standard.py +2 -2
  21. omlish/marshal/trivial/__init__.py +0 -0
  22. omlish/marshal/{forbidden.py → trivial/forbidden.py} +7 -7
  23. omlish/marshal/{nop.py → trivial/nop.py} +5 -5
  24. omlish/os/deathpacts/__init__.py +15 -0
  25. omlish/os/deathpacts/base.py +76 -0
  26. omlish/os/deathpacts/heartbeatfile.py +85 -0
  27. omlish/os/{death.py → deathpacts/pipe.py} +20 -90
  28. omlish/os/forkhooks.py +55 -31
  29. omlish/os/pidfiles/manager.py +11 -44
  30. omlish/reflect/__init__.py +1 -0
  31. omlish/reflect/inspect.py +43 -0
  32. omlish/sql/queries/__init__.py +4 -4
  33. omlish/sql/queries/rendering2.py +248 -0
  34. omlish/text/parts.py +26 -23
  35. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev230.dist-info}/METADATA +1 -1
  36. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev230.dist-info}/RECORD +46 -40
  37. omlish/formats/json/delimted.py +0 -4
  38. /omlish/collections/{persistent.py → persistent/persistent.py} +0 -0
  39. /omlish/collections/{treap.py → persistent/treap.py} +0 -0
  40. /omlish/collections/{treapmap.py → persistent/treapmap.py} +0 -0
  41. /omlish/marshal/{utils.py → proxy.py} +0 -0
  42. /omlish/marshal/{singular → trivial}/any.py +0 -0
  43. /omlish/sql/queries/{building.py → std.py} +0 -0
  44. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev230.dist-info}/LICENSE +0 -0
  45. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev230.dist-info}/WHEEL +0 -0
  46. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev230.dist-info}/entry_points.txt +0 -0
  47. {omlish-0.0.0.dev229.dist-info → omlish-0.0.0.dev230.dist-info}/top_level.txt +0 -0
@@ -1,84 +1,11 @@
1
- import abc
2
1
  import os
3
- import signal
4
- import sys
5
- import time
6
2
  import typing as ta
7
3
  import weakref
8
4
 
9
- from .. import check
10
- from .forkhooks import ForkHook
11
- from .forkhooks import get_fork_depth
12
-
13
-
14
- ##
15
-
16
-
17
- class Deathpact(abc.ABC):
18
- @abc.abstractmethod
19
- def poll(self) -> None:
20
- raise NotImplementedError
21
-
22
-
23
- class NopDeathpact(Deathpact):
24
- def poll(self) -> None:
25
- pass
26
-
27
-
28
- ##
29
-
30
-
31
- class BaseDeathpact(Deathpact, abc.ABC):
32
- def __init__(
33
- self,
34
- *,
35
- interval_s: float = .5,
36
- signal: int | None = signal.SIGTERM,
37
- output: ta.Literal['stdout', 'stderr'] | None = 'stderr',
38
- on_die: ta.Callable[[], None] | None = None,
39
- ) -> None:
40
- super().__init__()
41
-
42
- self._interval_s = interval_s
43
- self._signal = signal
44
- self._output = output
45
- self._on_die = on_die
46
-
47
- self._last_check_t: float | None = None
48
-
49
- def _print(self, msg: str) -> None:
50
- match self._output:
51
- case 'stdout':
52
- f = sys.stdout
53
- case 'stderr':
54
- f = sys.stderr
55
- case _:
56
- return
57
- print(f'{self} pid={os.getpid()}: {msg}', file=f)
58
-
59
- def die(self) -> None:
60
- self._print('Triggered! Process terminating!')
61
-
62
- if self._on_die is not None:
63
- self._on_die()
64
-
65
- if self._signal is not None:
66
- os.kill(os.getpid(), self._signal)
67
-
68
- sys.exit(1)
69
-
70
- @abc.abstractmethod
71
- def should_die(self) -> bool:
72
- raise NotImplementedError
73
-
74
- def maybe_die(self) -> None:
75
- if self.should_die():
76
- self.die()
77
-
78
- def poll(self) -> None:
79
- if self._last_check_t is None or (time.monotonic() - self._last_check_t) >= self._interval_s:
80
- self.maybe_die()
81
- self._last_check_t = time.monotonic()
5
+ from ... import check
6
+ from ..forkhooks import ForkHook
7
+ from ..forkhooks import ProcessOriginTracker
8
+ from .base import BaseDeathpact
82
9
 
83
10
 
84
11
  ##
@@ -92,16 +19,13 @@ class PipeDeathpact(BaseDeathpact):
92
19
  handle such cases.
93
20
  """
94
21
 
95
- _COOKIE: ta.ClassVar[bytes] = os.urandom(16)
96
-
97
22
  def __init__(self, **kwargs: ta.Any) -> None:
98
23
  super().__init__(**kwargs)
99
24
 
100
25
  self._rfd: int | None = None
101
26
  self._wfd: int | None = None
102
27
 
103
- self._cookie: bytes | None = self._COOKIE
104
- self._fork_depth: int | None = get_fork_depth()
28
+ self._process_origin = ProcessOriginTracker()
105
29
 
106
30
  def __repr__(self) -> str:
107
31
  return f'{self.__class__.__name__}(rfd={self._rfd}, wfd={self._wfd})'
@@ -111,11 +35,13 @@ class PipeDeathpact(BaseDeathpact):
111
35
  return check.not_none(self._rfd)
112
36
 
113
37
  def is_parent(self) -> bool:
114
- return (self._COOKIE, get_fork_depth()) == (self._cookie, self._fork_depth)
38
+ return self._process_origin.is_in_origin_process()
115
39
 
116
40
  #
117
41
 
118
42
  def __enter__(self) -> ta.Self:
43
+ check.state(self.is_parent())
44
+
119
45
  check.none(self._rfd)
120
46
  check.none(self._wfd)
121
47
 
@@ -125,18 +51,24 @@ class PipeDeathpact(BaseDeathpact):
125
51
 
126
52
  return self
127
53
 
128
- def _close_wfd_if_not_parent(self) -> None:
54
+ def close(self) -> None:
55
+ if self._rfd is not None:
56
+ os.close(self._rfd)
57
+ self._rfd = None
58
+
129
59
  if self._wfd is not None:
130
- if not self.is_parent():
60
+ if self.is_parent():
131
61
  os.close(check.not_none(self._wfd))
132
62
  self._wfd = None
133
63
 
134
64
  def __exit__(self, exc_type, exc_val, exc_tb):
135
- if self._rfd is not None:
136
- os.close(self._rfd)
137
- self._rfd = None
65
+ self.close()
138
66
 
139
- self._close_wfd_if_not_parent()
67
+ def _close_wfd_if_not_parent(self) -> None:
68
+ if self._wfd is not None:
69
+ if not self.is_parent():
70
+ os.close(check.not_none(self._wfd))
71
+ self._wfd = None
140
72
 
141
73
  #
142
74
 
@@ -145,8 +77,6 @@ class PipeDeathpact(BaseDeathpact):
145
77
  **self.__dict__,
146
78
  **dict(
147
79
  _wfd=None,
148
- _cookie=None,
149
- _fork_depth=None,
150
80
  ),
151
81
  }
152
82
 
omlish/os/forkhooks.py CHANGED
@@ -1,9 +1,5 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  # @omlish-lite
3
- """
4
- TODO:
5
- - ForkHook base class? all classmethods? prevents pickling
6
- """
7
3
  import abc
8
4
  import os
9
5
  import threading
@@ -29,31 +25,6 @@ class _ForkHookManager:
29
25
 
30
26
  #
31
27
 
32
- _installed: ta.ClassVar[bool] = False
33
-
34
- @classmethod
35
- def _install(cls) -> bool:
36
- if cls._installed:
37
- return False
38
-
39
- check.state(not cls._installed)
40
-
41
- os.register_at_fork(
42
- before=cls._before_fork,
43
- after_in_parent=cls._after_fork_in_parent,
44
- after_in_child=cls._after_fork_in_child,
45
- )
46
-
47
- cls._installed = True
48
- return True
49
-
50
- @classmethod
51
- def install(cls) -> bool:
52
- with cls._lock:
53
- return cls._install()
54
-
55
- #
56
-
57
28
  class Hook(ta.NamedTuple):
58
29
  key: ta.Any
59
30
  priority: int
@@ -74,8 +45,8 @@ class _ForkHookManager:
74
45
  def _rebuild_hook_collections(cls) -> None:
75
46
  cls._hook_keys = frozenset(cls._hooks_by_key)
76
47
 
77
- # Uses on dict order preservation for insertion-order of hooks of equal priority (although that shouldn't be
78
- # depended upon for usecase correctness.
48
+ # Uses dict order preservation to retain insertion-order of hooks of equal priority (although that shouldn't be
49
+ # depended upon for usecase correctness).
79
50
  cls._priority_ordered_hooks = sorted(cls._hooks_by_key.values(), key=lambda h: h.priority)
80
51
 
81
52
  #
@@ -121,6 +92,31 @@ class _ForkHookManager:
121
92
 
122
93
  #
123
94
 
95
+ _installed: ta.ClassVar[bool] = False
96
+
97
+ @classmethod
98
+ def _install(cls) -> bool:
99
+ if cls._installed:
100
+ return False
101
+
102
+ check.state(not cls._installed)
103
+
104
+ os.register_at_fork(
105
+ before=cls._before_fork,
106
+ after_in_parent=cls._after_fork_in_parent,
107
+ after_in_child=cls._after_fork_in_child,
108
+ )
109
+
110
+ cls._installed = True
111
+ return True
112
+
113
+ @classmethod
114
+ def install(cls) -> bool:
115
+ with cls._lock:
116
+ return cls._install()
117
+
118
+ #
119
+
124
120
  @classmethod
125
121
  def _before_fork(cls) -> None:
126
122
  cls._lock.acquire()
@@ -213,3 +209,31 @@ class _ForkDepthTracker(ForkHook):
213
209
 
214
210
  def get_fork_depth() -> int:
215
211
  return _ForkDepthTracker.get_fork_depth()
212
+
213
+
214
+ ##
215
+
216
+
217
+ class ProcessOriginTracker:
218
+ _PROCESS_COOKIE: ta.ClassVar[bytes] = os.urandom(16)
219
+
220
+ def __init__(self, **kwargs: ta.Any) -> None:
221
+ super().__init__(**kwargs)
222
+
223
+ self._process_cookie: bytes | None = self._PROCESS_COOKIE
224
+ self._fork_depth: int | None = get_fork_depth()
225
+
226
+ def is_in_origin_process(self) -> bool:
227
+ return (self._PROCESS_COOKIE, get_fork_depth()) == (self._process_cookie, self._fork_depth)
228
+
229
+ def __getstate__(self):
230
+ return {
231
+ **self.__dict__,
232
+ **dict(
233
+ _cookie=None,
234
+ _fork_depth=None,
235
+ ),
236
+ }
237
+
238
+ def __setstate__(self, state):
239
+ self.__dict__.update(state)
@@ -7,71 +7,38 @@ import typing as ta
7
7
  import weakref
8
8
 
9
9
  from ...lite.check import check
10
+ from ..forkhooks import ForkHook
10
11
  from .pidfile import Pidfile
11
12
 
12
13
 
13
14
  ##
14
15
 
15
16
 
16
- class _PidfileManager:
17
+ class _PidfileManager(ForkHook):
17
18
  """
18
19
  Manager for controlled inheritance of Pidfiles across forks in the presence of multiple threads. There is of course
19
20
  no safe or correct way to mix the use of fork and multiple active threads, and one should never write code which
20
21
  does so, but in the Real World one may still find oneself in such a situation outside of their control (such as when
21
22
  running under Pycharm's debugger which forces the use of forked multiprocessing).
22
-
23
- Not implemented as an instantiated class as there is no way to unregister at_fork listeners, and because Pidfiles
24
- may be pickled and there must be no possibility of accidentally unpickling and instantiating a new instance of the
25
- manager.
26
23
  """
27
24
 
28
- def __new__(cls, *args, **kwargs): # noqa
29
- raise TypeError
30
-
31
25
  _lock: ta.ClassVar[threading.Lock] = threading.Lock()
32
- _installed: ta.ClassVar[bool] = False
33
26
  _pidfile_threads: ta.ClassVar[ta.MutableMapping[Pidfile, threading.Thread]] = weakref.WeakKeyDictionary()
27
+ _num_kills: ta.ClassVar[int] = 0
34
28
 
35
29
  @classmethod
36
- def _before_fork(cls) -> None:
37
- cls._lock.acquire()
38
-
39
- @classmethod
40
- def _after_fork_in_parent(cls) -> None:
41
- cls._lock.release()
30
+ def num_kills(cls) -> int:
31
+ return cls._num_kills
42
32
 
43
33
  @classmethod
44
34
  def _after_fork_in_child(cls) -> None:
45
- th = threading.current_thread()
46
- for pf, pf_th in list(cls._pidfile_threads.items()):
47
- if pf_th is not th:
48
- pf.close()
49
- del cls._pidfile_threads[pf]
50
-
51
- cls._lock.release()
52
-
53
- #
54
-
55
- @classmethod
56
- def _install(cls) -> None:
57
- check.state(not cls._installed)
58
-
59
- os.register_at_fork(
60
- before=cls._before_fork,
61
- after_in_parent=cls._after_fork_in_parent,
62
- after_in_child=cls._after_fork_in_child,
63
- )
64
-
65
- cls._installed = True
66
-
67
- @classmethod
68
- def install(cls) -> bool:
69
35
  with cls._lock:
70
- if cls._installed:
71
- return False
72
-
73
- cls._install()
74
- return True
36
+ th = threading.current_thread()
37
+ for pf, pf_th in list(cls._pidfile_threads.items()):
38
+ if pf_th is not th:
39
+ pf.close()
40
+ del cls._pidfile_threads[pf]
41
+ cls._num_kills += 1
75
42
 
76
43
  @classmethod
77
44
  @contextlib.contextmanager
@@ -1,5 +1,6 @@
1
1
  from .inspect import ( # noqa
2
2
  get_annotations,
3
+ get_filtered_type_hints,
3
4
  )
4
5
 
5
6
  from .ops import ( # noqa
omlish/reflect/inspect.py CHANGED
@@ -4,6 +4,7 @@ TODO:
4
4
  """
5
5
  import typing as ta
6
6
 
7
+ from .. import check
7
8
  from .. import lang
8
9
 
9
10
 
@@ -16,8 +17,50 @@ else:
16
17
  annotationlib = lang.lazy_import('annotationlib', optional=True, cache_failure=True)
17
18
 
18
19
 
20
+ ##
21
+
22
+
19
23
  def get_annotations(obj: ta.Any) -> ta.Mapping[str, ta.Any]:
20
24
  if (al := annotationlib()) is not None:
21
25
  return al.get_annotations(obj, format=al.Format.FORWARDREF) # noqa
22
26
  else:
23
27
  return inspect.get_annotations(obj)
28
+
29
+
30
+ ##
31
+
32
+
33
+ class _TypeHintsTarget:
34
+ def __init__(self, obj, annotations):
35
+ super().__init__()
36
+
37
+ self.obj = obj
38
+ self.__annotations__ = annotations
39
+ self.__globals__ = obj.__globals__
40
+ self.__type_params__ = obj.__type_params__
41
+
42
+
43
+ def get_filtered_type_hints(
44
+ obj: ta.Any,
45
+ *,
46
+ include: ta.Container[str] | None = None,
47
+ exclude: ta.Container[str] | None = None,
48
+ ) -> ta.Mapping[str, ta.Any]:
49
+ """
50
+ Gross hack: get_type_hints doesn't support recursive types, but it's nice to support functions with, at least,
51
+ recursive return types.
52
+
53
+ This is consciously constrained in what it supports - basically just functions.
54
+ """
55
+
56
+ check.not_isinstance(include, str)
57
+ check.not_isinstance(exclude, str)
58
+
59
+ anns = {
60
+ k: v
61
+ for k, v in obj.__annotations__.items()
62
+ if (include is None or k in include)
63
+ and not (exclude is not None and k in exclude)
64
+ }
65
+
66
+ return ta.get_type_hints(_TypeHintsTarget(obj, anns))
@@ -11,10 +11,6 @@ from .binary import ( # noqa
11
11
  BinaryOps,
12
12
  )
13
13
 
14
- from .building import ( # noqa
15
- StdBuilder,
16
- )
17
-
18
14
  from .exprs import ( # noqa
19
15
  CanExpr,
20
16
  CanLiteral,
@@ -76,6 +72,10 @@ from .selects import ( # noqa
76
72
  SelectItem,
77
73
  )
78
74
 
75
+ from .std import ( # noqa
76
+ StdBuilder,
77
+ )
78
+
79
79
  from .stmts import ( # noqa
80
80
  CanExpr,
81
81
  ExprStmt,
@@ -0,0 +1,248 @@
1
+ """
2
+ TODO:
3
+ - minimal parens
4
+ - text.parts
5
+ - QuoteStyle
6
+ - ParamStyle
7
+
8
+ ==
9
+
10
+ def needs_parens(self, e: Expr) -> bool:
11
+ if isinstance(e, (Literal, Ident, Name)):
12
+ return True
13
+ elif isinstance(e, Expr):
14
+ return False
15
+ else:
16
+ raise TypeError(e)
17
+ """
18
+ import dataclasses as dc
19
+ import typing as ta
20
+
21
+ from ... import dispatch
22
+ from ... import lang
23
+ from ...text import parts as tp
24
+ from ..params import ParamStyle
25
+ from ..params import make_params_preparer
26
+ from .base import Node
27
+ from .binary import Binary
28
+ from .binary import BinaryOp
29
+ from .binary import BinaryOps
30
+ from .exprs import Literal
31
+ from .exprs import NameExpr
32
+ from .idents import Ident
33
+ from .inserts import Insert
34
+ from .inserts import Values
35
+ from .multi import Multi
36
+ from .multi import MultiKind
37
+ from .names import Name
38
+ from .params import Param
39
+ from .relations import Join
40
+ from .relations import JoinKind
41
+ from .relations import Table
42
+ from .selects import Select
43
+ from .selects import SelectItem
44
+ from .unary import Unary
45
+ from .unary import UnaryOp
46
+ from .unary import UnaryOps
47
+
48
+
49
+ @dc.dataclass(frozen=True)
50
+ class RenderedQuery(lang.Final):
51
+ t: tp.Part
52
+ args: lang.Args
53
+
54
+
55
+ class Renderer(lang.Abstract):
56
+ def __init__(
57
+ self,
58
+ *,
59
+ param_style: ParamStyle | None = None,
60
+ ) -> None:
61
+ super().__init__()
62
+
63
+ self._param_style = param_style if param_style is not None else self.default_param_style
64
+
65
+ self._params_preparer = make_params_preparer(self._param_style)
66
+
67
+ default_param_style: ta.ClassVar[ParamStyle] = ParamStyle.PYFORMAT
68
+
69
+ def args(self) -> lang.Args:
70
+ return self._params_preparer.prepare()
71
+
72
+ @dispatch.method
73
+ def render(self, o: ta.Any) -> tp.Part:
74
+ raise TypeError(o)
75
+
76
+
77
+ class StdRenderer(Renderer):
78
+ # parens
79
+
80
+ NEEDS_PAREN_TYPES: ta.AbstractSet[type[Node]] = {
81
+ Binary,
82
+ # IsNull,
83
+ # SelectExpr,
84
+ }
85
+
86
+ def needs_paren(self, node: Node) -> bool:
87
+ return type(node) in self.NEEDS_PAREN_TYPES
88
+
89
+ def paren(self, node: Node) -> tp.Part:
90
+ return tp.Wrap(self.render(node)) if self.needs_paren(node) else self.render(node)
91
+
92
+ # binary
93
+
94
+ BINARY_OP_TO_STR: ta.ClassVar[ta.Mapping[BinaryOp, str]] = {
95
+ BinaryOps.EQ: '=',
96
+ BinaryOps.NE: '!=',
97
+ BinaryOps.LT: '<',
98
+ BinaryOps.LE: '<=',
99
+ BinaryOps.GT: '>',
100
+ BinaryOps.GE: '>=',
101
+
102
+ BinaryOps.ADD: '+',
103
+ BinaryOps.SUB: '-',
104
+ BinaryOps.MUL: '*',
105
+ BinaryOps.DIV: '/',
106
+ BinaryOps.MOD: '%',
107
+
108
+ BinaryOps.CONCAT: '||',
109
+ }
110
+
111
+ @Renderer.render.register
112
+ def render_binary(self, o: Binary) -> tp.Part:
113
+ return [
114
+ self.paren(o.l),
115
+ self.BINARY_OP_TO_STR[o.op],
116
+ self.paren(o.r),
117
+ ]
118
+
119
+ # exprs
120
+
121
+ @Renderer.render.register
122
+ def render_literal(self, o: Literal) -> tp.Part:
123
+ return repr(o.v)
124
+
125
+ @Renderer.render.register
126
+ def render_name_expr(self, o: NameExpr) -> tp.Part:
127
+ return self.render(o.n)
128
+
129
+ @Renderer.render.register
130
+ def render_param(self, o: Param) -> tp.Part:
131
+ return self._params_preparer.add(o.n if o.n is not None else id(o))
132
+
133
+ # idents
134
+
135
+ @Renderer.render.register
136
+ def render_ident(self, o: Ident) -> tp.Part:
137
+ return f'"{o.s}"'
138
+
139
+ # inserts
140
+
141
+ @Renderer.render.register
142
+ def render_values(self, o: Values) -> tp.Part:
143
+ return [
144
+ 'values',
145
+ tp.Wrap(tp.List([self.render(v) for v in o.vs])),
146
+ ]
147
+
148
+ @Renderer.render.register
149
+ def render_insert(self, o: Insert) -> tp.Part:
150
+ return [
151
+ 'insert into',
152
+ self.render(o.into),
153
+ tp.Wrap(tp.List([self.render(c) for c in o.columns])),
154
+ ]
155
+
156
+ # multis
157
+
158
+ MULTI_KIND_TO_STR: ta.ClassVar[ta.Mapping[MultiKind, str]] = {
159
+ MultiKind.AND: 'and',
160
+ MultiKind.OR: 'or',
161
+ }
162
+
163
+ @Renderer.render.register
164
+ def render_multi(self, o: Multi) -> tp.Part:
165
+ return tp.Wrap(tp.List(
166
+ [self.render(e) for e in o.es],
167
+ delimiter=' ' + self.MULTI_KIND_TO_STR[o.k], # FIXME: Part
168
+ ))
169
+
170
+ # names
171
+
172
+ @Renderer.render.register
173
+ def render_name(self, o: Name) -> tp.Part:
174
+ out: list[tp.Part] = []
175
+ for n, i in enumerate(o.ps):
176
+ if n:
177
+ out.append('.')
178
+ out.append(self.render(i))
179
+ return tp.Concat(out)
180
+
181
+ # relations
182
+
183
+ @Renderer.render.register
184
+ def render_table(self, o: Table) -> tp.Part:
185
+ return [
186
+ self.render(o.n),
187
+ *(['as', self.render(o.a)] if o.a is not None else []),
188
+ ]
189
+
190
+ JOIN_KIND_TO_STR: ta.ClassVar[ta.Mapping[JoinKind, str]] = {
191
+ JoinKind.DEFAULT: 'join',
192
+ JoinKind.INNER: 'inner join',
193
+ JoinKind.LEFT: 'left join',
194
+ JoinKind.LEFT_OUTER: 'left outer join',
195
+ JoinKind.RIGHT: 'right join',
196
+ JoinKind.RIGHT_OUTER: 'right outer join',
197
+ JoinKind.FULL: 'full join',
198
+ JoinKind.FULL_OUTER: 'full outer join',
199
+ JoinKind.CROSS: 'cross join',
200
+ JoinKind.NATURAL: 'natural join',
201
+ }
202
+
203
+ @Renderer.render.register
204
+ def render_join(self, o: Join) -> tp.Part:
205
+ return [
206
+ self.render(o.l),
207
+ self.JOIN_KIND_TO_STR[o.k],
208
+ self.render(o.r),
209
+ *(['on', self.render(o.c)] if o.c is not None else []),
210
+ ]
211
+
212
+ # selects
213
+
214
+ @Renderer.render.register
215
+ def render_select_item(self, o: SelectItem) -> tp.Part:
216
+ return [
217
+ self.render(o.v),
218
+ *(['as', self.render(o.a)] if o.a is not None else []),
219
+ ]
220
+
221
+ @Renderer.render.register
222
+ def render_select(self, o: Select) -> tp.Part:
223
+ return [
224
+ 'select',
225
+ tp.List([self.render(i) for i in o.items]),
226
+ *(['from', self.render(o.from_)] if o.from_ is not None else []),
227
+ *(['where', self.render(o.where)] if o.where is not None else []),
228
+ ]
229
+
230
+ # unary
231
+
232
+ UNARY_OP_TO_STR: ta.ClassVar[ta.Mapping[UnaryOp, tuple[str, str]]] = {
233
+ UnaryOps.NOT: ('not ', ''),
234
+ UnaryOps.IS_NULL: ('', ' is null'),
235
+ UnaryOps.IS_NOT_NULL: ('', ' is not null'),
236
+
237
+ UnaryOps.POS: ('+', ''),
238
+ UnaryOps.NEG: ('-', ''),
239
+ }
240
+
241
+ @Renderer.render.register
242
+ def render_unary(self, o: Unary) -> tp.Part:
243
+ pfx, sfx = self.UNARY_OP_TO_STR[o.op]
244
+ return tp.Concat([
245
+ pfx,
246
+ self.render(o.v),
247
+ sfx,
248
+ ])