omlish 0.0.0.dev86__py3-none-any.whl → 0.0.0.dev88__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev86'
2
- __revision__ = '39be25efaa0206ceb3478be1878e14671f5940f1'
1
+ __version__ = '0.0.0.dev88'
2
+ __revision__ = 'ccf0f3b0ac6cc9e9ca522c8c4a9c8e6a4151bd7b'
3
3
 
4
4
 
5
5
  #
@@ -168,10 +168,9 @@ def _main() -> None:
168
168
  else:
169
169
  renderer = StreamJsonRenderer(
170
170
  out,
171
- StreamJsonRenderer.Options(
172
- **kw,
173
- style=term_color if args.color else None,
174
- ),
171
+ style=term_color if args.color else None,
172
+ delimiter='\n',
173
+ **kw,
175
174
  )
176
175
  build = None
177
176
 
@@ -198,7 +197,8 @@ def _main() -> None:
198
197
  if not buf:
199
198
  break
200
199
 
201
- out.write('\n')
200
+ if renderer is not None:
201
+ out.write('\n')
202
202
 
203
203
  else:
204
204
  with io.TextIOWrapper(in_file) as tw:
@@ -1,5 +1,4 @@
1
1
  import abc
2
- import dataclasses as dc
3
2
  import enum
4
3
  import io
5
4
  import json
@@ -20,35 +19,32 @@ class AbstractJsonRenderer(lang.Abstract, ta.Generic[I]):
20
19
  VALUE = enum.auto()
21
20
  KEY = enum.auto()
22
21
 
23
- @dc.dataclass(frozen=True, kw_only=True)
24
- class Options:
25
- indent: int | str | None = None
26
- separators: tuple[str, str] | None = None
27
- sort_keys: bool = False
28
- style: ta.Callable[[ta.Any, 'AbstractJsonRenderer.State'], tuple[str, str]] | None = None
29
-
30
22
  def __init__(
31
23
  self,
32
24
  out: JsonRendererOut,
33
- opts: Options = Options(),
25
+ *,
26
+ indent: int | str | None = None,
27
+ separators: tuple[str, str] | None = None,
28
+ sort_keys: bool = False,
29
+ style: ta.Callable[[ta.Any, State], tuple[str, str]] | None = None,
34
30
  ) -> None:
35
31
  super().__init__()
36
32
 
37
33
  self._out = out
38
- self._opts = opts
34
+ self._sort_keys = sort_keys
35
+ self._style = style
39
36
 
40
- separators = opts.separators
41
- if isinstance(opts.indent, (str, int)):
42
- self._indent = (' ' * opts.indent) if isinstance(opts.indent, int) else opts.indent
37
+ if isinstance(indent, (str, int)):
38
+ self._indent = (' ' * indent) if isinstance(indent, int) else indent
43
39
  self._endl = '\n'
44
40
  if separators is None:
45
41
  separators = (',', ': ')
46
- elif opts.indent is None:
42
+ elif indent is None:
47
43
  self._indent = self._endl = ''
48
44
  if separators is None:
49
45
  separators = (', ', ': ')
50
46
  else:
51
- raise TypeError(opts.indent)
47
+ raise TypeError(indent)
52
48
  self._comma, self._colon = separators
53
49
 
54
50
  self._level = 0
@@ -76,7 +72,7 @@ class AbstractJsonRenderer(lang.Abstract, ta.Generic[I]):
76
72
  @classmethod
77
73
  def render_str(cls, i: I, **kwargs: ta.Any) -> str:
78
74
  out = io.StringIO()
79
- cls(out, cls.Options(**kwargs)).render(i)
75
+ cls(out, **kwargs).render(i)
80
76
  return out.getvalue()
81
77
 
82
78
 
@@ -86,8 +82,8 @@ class JsonRenderer(AbstractJsonRenderer[ta.Any]):
86
82
  o: ta.Any,
87
83
  state: AbstractJsonRenderer.State = AbstractJsonRenderer.State.VALUE,
88
84
  ) -> None:
89
- if self._opts.style is not None:
90
- pre, post = self._opts.style(o, state)
85
+ if self._style is not None:
86
+ pre, post = self._style(o, state)
91
87
  self._write(pre)
92
88
  else:
93
89
  post = None
@@ -102,7 +98,7 @@ class JsonRenderer(AbstractJsonRenderer[ta.Any]):
102
98
  self._write('{')
103
99
  self._level += 1
104
100
  items = list(o.items())
105
- if self._opts.sort_keys:
101
+ if self._sort_keys:
106
102
  items.sort(key=lambda t: t[0])
107
103
  for i, (k, v) in enumerate(items):
108
104
  if i:
@@ -94,7 +94,7 @@ CONST_TOKENS: ta.Mapping[str, tuple[TokenKind, str | float | None]] = {
94
94
  ##
95
95
 
96
96
 
97
- @dc.dataclass(frozen=True)
97
+ @dc.dataclass()
98
98
  class JsonLexError(Exception):
99
99
  message: str
100
100
 
@@ -3,7 +3,6 @@ import typing as ta
3
3
 
4
4
  from ..render import AbstractJsonRenderer
5
5
  from ..render import JsonRendererOut
6
- from .build import JsonObjectBuilder
7
6
  from .parse import BeginArray
8
7
  from .parse import BeginObject
9
8
  from .parse import EndArray
@@ -19,23 +18,28 @@ class StreamJsonRenderer(AbstractJsonRenderer[ta.Iterable[JsonStreamParserEvent]
19
18
  def __init__(
20
19
  self,
21
20
  out: JsonRendererOut,
22
- opts: AbstractJsonRenderer.Options = AbstractJsonRenderer.Options(),
21
+ *,
22
+ delimiter: str = '',
23
+ sort_keys: bool = False,
24
+ **kwargs: ta.Any,
23
25
  ) -> None:
24
- if opts.sort_keys:
26
+ if sort_keys:
25
27
  raise TypeError('Not yet implemented')
26
28
 
27
- super().__init__(out, opts)
29
+ self._delimiter = delimiter
30
+
31
+ super().__init__(out, **kwargs)
28
32
 
29
33
  self._stack: list[tuple[ta.Literal['OBJECT', 'ARRAY'], int]] = []
30
- self._builder: JsonObjectBuilder | None = None
34
+ self._need_delimit = False
31
35
 
32
36
  def _render_value(
33
37
  self,
34
38
  o: ta.Any,
35
39
  state: AbstractJsonRenderer.State = AbstractJsonRenderer.State.VALUE,
36
40
  ) -> None:
37
- if self._opts.style is not None:
38
- pre, post = self._opts.style(o, state)
41
+ if self._style is not None:
42
+ pre, post = self._style(o, state)
39
43
  self._write(pre)
40
44
  else:
41
45
  post = None
@@ -53,6 +57,10 @@ class StreamJsonRenderer(AbstractJsonRenderer[ta.Iterable[JsonStreamParserEvent]
53
57
  self._write(post)
54
58
 
55
59
  def _render(self, e: JsonStreamParserEvent) -> None:
60
+ if self._need_delimit:
61
+ self._write(self._delimiter)
62
+ self._need_delimit = False
63
+
56
64
  if e != EndArray and self._stack and (tt := self._stack[-1])[0] == 'ARRAY':
57
65
  if tt[1]:
58
66
  self._write(self._comma)
@@ -64,6 +72,8 @@ class StreamJsonRenderer(AbstractJsonRenderer[ta.Iterable[JsonStreamParserEvent]
64
72
 
65
73
  if e is None or isinstance(e, (str, int, float, bool)):
66
74
  self._render_value(e)
75
+ if not self._stack:
76
+ self._need_delimit = True
67
77
 
68
78
  #
69
79
 
@@ -92,6 +102,8 @@ class StreamJsonRenderer(AbstractJsonRenderer[ta.Iterable[JsonStreamParserEvent]
92
102
  if tt[1]:
93
103
  self._write_indent()
94
104
  self._write('}')
105
+ if not self._stack:
106
+ self._need_delimit = True
95
107
 
96
108
  #
97
109
 
@@ -108,6 +120,8 @@ class StreamJsonRenderer(AbstractJsonRenderer[ta.Iterable[JsonStreamParserEvent]
108
120
  if tt[1]:
109
121
  self._write_indent()
110
122
  self._write(']')
123
+ if not self._stack:
124
+ self._need_delimit = True
111
125
 
112
126
  #
113
127
 
omlish/sql/_abc.py CHANGED
@@ -1,20 +1,20 @@
1
1
  import typing as ta
2
2
 
3
3
 
4
- DBAPITypeCode: ta.TypeAlias = ta.Any | None
5
-
6
- DBAPIColumnDescription: ta.TypeAlias = tuple[
7
- str,
8
- DBAPITypeCode,
9
- int | None,
10
- int | None,
11
- int | None,
12
- int | None,
13
- bool | None,
4
+ DbapiTypeCode: ta.TypeAlias = ta.Any | None
5
+
6
+ DbapiColumnDescription: ta.TypeAlias = tuple[
7
+ str, # name
8
+ DbapiTypeCode, # type_code
9
+ int | None, # display_size
10
+ int | None, # internal_size
11
+ int | None, # precision
12
+ int | None, # scale
13
+ bool | None, # null_ok
14
14
  ]
15
15
 
16
16
 
17
- class DBAPIConnection(ta.Protocol):
17
+ class DbapiConnection(ta.Protocol):
18
18
  def close(self) -> object: ...
19
19
 
20
20
  def commit(self) -> object: ...
@@ -22,12 +22,12 @@ class DBAPIConnection(ta.Protocol):
22
22
  # optional:
23
23
  # def rollback(self) -> ta.Any: ...
24
24
 
25
- def cursor(self) -> 'DBAPICursor': ...
25
+ def cursor(self) -> 'DbapiCursor': ...
26
26
 
27
27
 
28
- class DBAPICursor(ta.Protocol):
28
+ class DbapiCursor(ta.Protocol):
29
29
  @property
30
- def description(self) -> ta.Sequence[DBAPIColumnDescription] | None: ...
30
+ def description(self) -> ta.Sequence[DbapiColumnDescription] | None: ...
31
31
 
32
32
  @property
33
33
  def rowcount(self) -> int: ...
@@ -60,6 +60,6 @@ class DBAPICursor(ta.Protocol):
60
60
 
61
61
  arraysize: int
62
62
 
63
- def setinputsizes(self, sizes: ta.Sequence[DBAPITypeCode | int | None]) -> object: ...
63
+ def setinputsizes(self, sizes: ta.Sequence[DbapiTypeCode | int | None]) -> object: ...
64
64
 
65
65
  def setoutputsize(self, size: int, column: int = ...) -> object: ...
omlish/sql/dbapi.py ADDED
@@ -0,0 +1,150 @@
1
+ """
2
+ https://peps.python.org/pep-0249/
3
+
4
+ ==
5
+
6
+ apilevel = '2.0'
7
+
8
+ threadsafety:
9
+ 0 - Threads may not share the module.
10
+ 1 - Threads may share the module, but not connections.
11
+ 2 - Threads may share the module and connections.
12
+ 3 - Threads may share the module, connections and cursors.
13
+
14
+ paramstyle:
15
+ qmark - Question mark style, e.g. ...WHERE name=?
16
+ numeric - Numeric, positional style, e.g. ...WHERE name=:1
17
+ named - Named style, e.g. ...WHERE name=:name
18
+ format - ANSI C printf format codes, e.g. ...WHERE name=%s
19
+ pyformat - Python extended format codes, e.g. ...WHERE name=%(name)s
20
+
21
+ Exception
22
+ | Warning
23
+ | Error
24
+ | InterfaceError
25
+ | DatabaseError
26
+ | DataError
27
+ | OperationalError
28
+ | IntegrityError
29
+ | InternalError
30
+ | ProgrammingError
31
+ | NotSupportedError
32
+
33
+ Date(year, month, day)
34
+ Time(hour, minute, second)
35
+ Timestamp(year, month, day, hour, minute, second)
36
+ DateFromTicks(ticks)
37
+ TimeFromTicks(ticks)
38
+ TimestampFromTicks(ticks)
39
+ Binary(string)
40
+
41
+ STRING type
42
+ BINARY type
43
+ NUMBER type
44
+ DATETIME type
45
+ ROWID type
46
+ """
47
+ import dataclasses as dc
48
+ import enum
49
+
50
+ from .. import lang
51
+ from .params import ParamStyle
52
+
53
+
54
+ class DbapiDialect(enum.Enum):
55
+ SQLITE = 'sqlite'
56
+ MYSQL = 'mysql'
57
+ POSTGRES = 'postgres'
58
+ SNOWFLAKE = 'snowflake'
59
+
60
+
61
+ @dc.dataclass(frozen=True, kw_only=True)
62
+ class DbapiDriver:
63
+ name: str
64
+ dialect: DbapiDialect
65
+ param_style: ParamStyle
66
+
67
+ package_name: str | None = None
68
+ module_name: str | None = None
69
+
70
+
71
+ class DbapiDrivers(lang.Namespace):
72
+
73
+ ##
74
+ # sqlite
75
+
76
+ SQLITE3 = DbapiDriver(
77
+ name='sqlite3',
78
+ dialect=DbapiDialect.SQLITE,
79
+ param_style=ParamStyle.QMARK,
80
+ )
81
+
82
+ SQLEAN = DbapiDriver(
83
+ name='sqlean',
84
+ dialect=DbapiDialect.SQLITE,
85
+ param_style=ParamStyle.QMARK,
86
+ package_name='sqlean.py',
87
+ )
88
+
89
+ DUCKDB = DbapiDriver(
90
+ name='duckdb',
91
+ dialect=DbapiDialect.SQLITE,
92
+ param_style=ParamStyle.QMARK,
93
+ )
94
+
95
+ ##
96
+ # mysql
97
+
98
+ PYMYSQL = DbapiDriver(
99
+ name='pymysql',
100
+ dialect=DbapiDialect.MYSQL,
101
+ param_style=ParamStyle.PYFORMAT,
102
+ )
103
+
104
+ MYSQL = DbapiDriver(
105
+ name='mysql',
106
+ dialect=DbapiDialect.MYSQL,
107
+ param_style=ParamStyle.PYFORMAT,
108
+ package_name='mysql-connector-python',
109
+ module_name='mysql.connector',
110
+ )
111
+
112
+ MYSQLCLIENT = DbapiDriver(
113
+ name='mysqlclient',
114
+ dialect=DbapiDialect.MYSQL,
115
+ param_style=ParamStyle.FORMAT,
116
+ package_name='mysqlclient',
117
+ module_name='MySQLdb',
118
+ )
119
+
120
+ ##
121
+ # postgres
122
+
123
+ PG8000 = DbapiDriver(
124
+ name='pg8000',
125
+ dialect=DbapiDialect.POSTGRES,
126
+ param_style=ParamStyle.FORMAT,
127
+ )
128
+
129
+ PSYCOPG2 = DbapiDriver(
130
+ name='psycopg2',
131
+ dialect=DbapiDialect.POSTGRES,
132
+ param_style=ParamStyle.PYFORMAT,
133
+ )
134
+
135
+ PSYCOPG = DbapiDriver(
136
+ name='psycopg',
137
+ dialect=DbapiDialect.POSTGRES,
138
+ param_style=ParamStyle.PYFORMAT,
139
+ )
140
+
141
+ ##
142
+ # snowflake
143
+
144
+ SNOWFLAKE = DbapiDriver(
145
+ name='snowflake',
146
+ dialect=DbapiDialect.SNOWFLAKE,
147
+ param_style=ParamStyle.PYFORMAT,
148
+ package_name='snowflake-connector-python',
149
+ module_name='snowflake.connector',
150
+ )
omlish/sql/params.py ADDED
@@ -0,0 +1,159 @@
1
+ import abc
2
+ import enum
3
+ import typing as ta
4
+
5
+ from .. import check
6
+ from .. import lang
7
+
8
+
9
+ ##
10
+
11
+
12
+ ParamKey: ta.TypeAlias = str | int
13
+
14
+
15
+ class ParamsPreparer(lang.Abstract):
16
+ @abc.abstractmethod
17
+ def add(self, k: ParamKey) -> str:
18
+ raise NotImplementedError
19
+
20
+ @abc.abstractmethod
21
+ def prepare(self) -> lang.Args:
22
+ raise NotImplementedError
23
+
24
+
25
+ class LinearParamsPreparer(ParamsPreparer):
26
+ def __init__(self, placeholder: str) -> None:
27
+ super().__init__()
28
+ self._placeholder = placeholder
29
+ self._args: list[ParamKey] = []
30
+
31
+ def add(self, k: ParamKey) -> str:
32
+ self._args.append(k)
33
+ return self._placeholder
34
+
35
+ def prepare(self) -> lang.Args:
36
+ return lang.Args(*self._args)
37
+
38
+
39
+ class NumericParamsPreparer(ParamsPreparer):
40
+ def __init__(self) -> None:
41
+ super().__init__()
42
+ self._args: list[ParamKey] = []
43
+ self._str_by_key: dict[ParamKey, str] = {}
44
+ self._pos_by_name: dict[str, int] = {}
45
+ self._pos_by_id: dict[int, int] = {}
46
+
47
+ def add(self, k: ParamKey) -> str:
48
+ try:
49
+ return self._str_by_key[k]
50
+ except KeyError:
51
+ pass
52
+
53
+ pos = len(self._args)
54
+ if isinstance(k, str):
55
+ check.not_in(k, self._pos_by_name)
56
+ self._pos_by_name[k] = pos
57
+ elif isinstance(k, int):
58
+ check.not_in(k, self._pos_by_id)
59
+ self._pos_by_id[k] = pos
60
+ else:
61
+ raise TypeError(f'Invalid key type: {type(k)}')
62
+
63
+ self._args.append(k)
64
+ ret = self._str_by_key[k] = f':{pos + 1}'
65
+ return ret
66
+
67
+ def prepare(self) -> lang.Args:
68
+ return lang.Args(*self._args)
69
+
70
+
71
+ class NamedParamsPreparer(ParamsPreparer):
72
+ def __init__(
73
+ self,
74
+ render: ta.Callable[[str], str],
75
+ *,
76
+ anon: ta.Callable[[int], str] | None = None,
77
+ ) -> None:
78
+ super().__init__()
79
+ self._render = render
80
+ self._anon = anon if anon is not None else self.underscore_anon
81
+ self._kwargs: dict[str, ParamKey] = {}
82
+ self._str_by_key: dict[ParamKey, str] = {}
83
+ self._kwargs_by_name: dict[str, str] = {}
84
+ self._kwargs_by_id: dict[int, str] = {}
85
+
86
+ @staticmethod
87
+ def render_named(kwarg: str) -> str:
88
+ return f':{kwarg}'
89
+
90
+ @staticmethod
91
+ def render_pyformat(kwarg: str) -> str:
92
+ return f'%({kwarg})s'
93
+
94
+ @staticmethod
95
+ def underscore_anon(n: int) -> str:
96
+ return f'_{n}'
97
+
98
+ def add(self, k: ParamKey) -> str:
99
+ try:
100
+ return self._str_by_key[k]
101
+ except KeyError:
102
+ pass
103
+
104
+ kwarg: str
105
+ if isinstance(k, str):
106
+ check.not_in(k, self._kwargs_by_name)
107
+ kwarg = k # TODO: collisions
108
+ self._kwargs_by_name[k] = kwarg
109
+ elif isinstance(k, int):
110
+ check.not_in(k, self._kwargs_by_id)
111
+ kwarg = self._anon(len(self._kwargs_by_id)) # TODO: collisions
112
+ self._kwargs_by_id[k] = kwarg
113
+ else:
114
+ raise TypeError(f'Invalid key type: {type(k)}')
115
+
116
+ self._kwargs[kwarg] = k
117
+ ret = self._str_by_key[k] = self._render(kwarg)
118
+ return ret
119
+
120
+ def prepare(self) -> lang.Args:
121
+ return lang.Args(**self._kwargs)
122
+
123
+
124
+ ##
125
+
126
+
127
+ class ParamStyle(enum.Enum):
128
+ QMARK = 'qmark' # Question mark style, e.g. ...WHERE name=?
129
+ FORMAT = 'format' # ANSI C printf format codes, e.g. ...WHERE name=%s
130
+
131
+ NUMERIC = 'numeric' # Numeric, positional style, e.g. ...WHERE name=:1
132
+
133
+ NAMED = 'named' # Named style, e.g. ...WHERE name=:name
134
+ PYFORMAT = 'pyformat' # Python extended format codes, e.g. ...WHERE name=%(name)s
135
+
136
+
137
+ def make_params_preparer(style: ParamStyle) -> ParamsPreparer:
138
+ if style is ParamStyle.QMARK:
139
+ return LinearParamsPreparer('?')
140
+ elif style is ParamStyle.FORMAT:
141
+ return LinearParamsPreparer('%s')
142
+ elif style is ParamStyle.NUMERIC:
143
+ return NumericParamsPreparer()
144
+ elif style is ParamStyle.NAMED:
145
+ return NamedParamsPreparer(NamedParamsPreparer.render_named)
146
+ elif style is ParamStyle.PYFORMAT:
147
+ return NamedParamsPreparer(NamedParamsPreparer.render_pyformat)
148
+ else:
149
+ raise ValueError(style)
150
+
151
+
152
+ ##
153
+
154
+
155
+ def substitute_params(args: lang.Args, values: ta.Mapping[ParamKey, ta.Any]) -> lang.Args:
156
+ return lang.Args(
157
+ *[values[a] for a in args.args],
158
+ **{k: values[v] for k, v in args.kwargs.items()},
159
+ )
@@ -18,11 +18,11 @@ from .stmts import Stmt
18
18
 
19
19
 
20
20
  class Values(dc.Frozen, lang.Final):
21
- vs: ta.Sequence[Expr]
21
+ vs: ta.Sequence[Expr] = dc.xfield(coerce=tuple)
22
22
 
23
23
 
24
24
  class Insert(Stmt, lang.Final):
25
- columns: ta.Sequence[Ident]
25
+ columns: ta.Sequence[Ident] = dc.xfield(coerce=tuple)
26
26
  into: Relation
27
27
  data: Values | Select
28
28
 
@@ -1,3 +1,7 @@
1
+ """
2
+ FIXME:
3
+ - refuse to marshal anon params
4
+ """
1
5
  import enum
2
6
  import typing as ta
3
7
 
@@ -15,17 +15,21 @@ def needs_parens(self, e: Expr) -> bool:
15
15
  else:
16
16
  raise TypeError(e)
17
17
  """
18
+ import dataclasses as dc
18
19
  import io
19
20
  import typing as ta
20
21
 
21
22
  from ... import dispatch
22
23
  from ... import lang
24
+ from ..params import ParamStyle
25
+ from ..params import make_params_preparer
23
26
  from .base import Node
24
27
  from .binary import Binary
25
28
  from .binary import BinaryOp
26
29
  from .binary import BinaryOps
27
30
  from .exprs import Literal
28
31
  from .exprs import NameExpr
32
+ from .exprs import Param
29
33
  from .idents import Ident
30
34
  from .inserts import Insert
31
35
  from .inserts import Values
@@ -40,20 +44,40 @@ from .unary import UnaryOp
40
44
  from .unary import UnaryOps
41
45
 
42
46
 
47
+ @dc.dataclass(frozen=True)
48
+ class RenderedQuery(lang.Final):
49
+ s: str
50
+ args: lang.Args
51
+
52
+
43
53
  class Renderer(lang.Abstract):
44
- def __init__(self, out: ta.TextIO) -> None:
54
+ def __init__(
55
+ self,
56
+ out: ta.TextIO,
57
+ *,
58
+ param_style: ParamStyle | None = None,
59
+ ) -> None:
45
60
  super().__init__()
46
61
  self._out = out
62
+ self._param_style = param_style if param_style is not None else self.default_param_style
63
+
64
+ self._params_preparer = make_params_preparer(self._param_style)
65
+
66
+ default_param_style: ta.ClassVar[ParamStyle] = ParamStyle.PYFORMAT
67
+
68
+ def args(self) -> lang.Args:
69
+ return self._params_preparer.prepare()
47
70
 
48
71
  @dispatch.method
49
- def render(self, o: ta.Any) -> None:
72
+ def render(self, o: ta.Any) -> str:
50
73
  raise TypeError(o)
51
74
 
52
75
  @classmethod
53
- def render_str(cls, o: ta.Any, *args: ta.Any, **kwargs: ta.Any) -> str:
76
+ def render_str(cls, o: ta.Any, *args: ta.Any, **kwargs: ta.Any) -> RenderedQuery:
54
77
  out = io.StringIO()
55
- cls(out, *args, **kwargs).render(o)
56
- return out.getvalue()
78
+ pp = cls(out, *args, **kwargs)
79
+ pp.render(o)
80
+ return RenderedQuery(out.getvalue(), pp.args())
57
81
 
58
82
 
59
83
  class StdRenderer(Renderer):
@@ -94,6 +118,10 @@ class StdRenderer(Renderer):
94
118
  def render_name_expr(self, o: NameExpr) -> None:
95
119
  self.render(o.n)
96
120
 
121
+ @Renderer.render.register
122
+ def render_param(self, o: Param) -> None:
123
+ self._out.write(self._params_preparer.add(o.n if o.n is not None else id(o)))
124
+
97
125
  # idents
98
126
 
99
127
  @Renderer.render.register
@@ -201,5 +229,5 @@ class StdRenderer(Renderer):
201
229
  self._out.write(sfx)
202
230
 
203
231
 
204
- def render(n: Node) -> str:
205
- return StdRenderer.render_str(n)
232
+ def render(n: Node, **kwargs: ta.Any) -> RenderedQuery:
233
+ return StdRenderer.render_str(n, **kwargs)
@@ -44,7 +44,7 @@ class SelectBuilder(ExprBuilder, RelationBuilder):
44
44
  where: CanExpr | None = None,
45
45
  ) -> Select:
46
46
  return Select(
47
- [self.select_item(i) for i in items],
47
+ tuple(self.select_item(i) for i in items),
48
48
  from_=self.relation(from_) if from_ is not None else None,
49
49
  where=self.expr(where) if where is not None else None,
50
50
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: omlish
3
- Version: 0.0.0.dev86
3
+ Version: 0.0.0.dev88
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause
@@ -1,5 +1,5 @@
1
1
  omlish/.manifests.json,sha256=hTFp9tvE72BxKloIq1s1SS0LRQlIsvMtO69Sbc47rKg,1704
2
- omlish/__about__.py,sha256=bmf1Xnu5T4--9ykGD63NIvhEZE9Fhk9w3IlmwLH40vU,3345
2
+ omlish/__about__.py,sha256=_pr15t5ifEAY1adRq4S8Fp21jhhRHPNo6mJlHz7_xBo,3345
3
3
  omlish/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  omlish/argparse.py,sha256=Dc73G8lyoQBLvXhMYUbzQUh4SJu_OTvKUXjSUxq_ang,7499
5
5
  omlish/c3.py,sha256=4vogWgwPb8TbNS2KkZxpoWbwjj7MuHG2lQG-hdtkvjI,8062
@@ -187,7 +187,7 @@ omlish/formats/json/__init__.py,sha256=xqW2APLGvCTO9dVTOlroR_AdrA5bCkdmUnbTkYHhJ
187
187
  omlish/formats/json/consts.py,sha256=u-x-qXqZvK0tWk3l3TrCTjk4mSjKmZ_ATdmd1hwHNAY,263
188
188
  omlish/formats/json/encoding.py,sha256=O4iIWle7W_-RwpOvJNlqOfkbnDyiQHexV5Za4hlrFzw,497
189
189
  omlish/formats/json/json.py,sha256=Mdqv2vdMi7gp96eV0BIYH5UdWpjWfsh-tSMZeywG-08,331
190
- omlish/formats/json/render.py,sha256=H6q1R3gL6ULAnfEhQRKDOzDJjWizdsRDYxIerBSEbq8,3797
190
+ omlish/formats/json/render.py,sha256=CRAh-DhfOcyjqM1DVBvZ22xl_h3jPPd_Hn0WqDYT3bo,3623
191
191
  omlish/formats/json/backends/__init__.py,sha256=gnaNDCxy_KmmPUPDnjxO5_WjuWxLGbI9FYWx8ZJuQUU,97
192
192
  omlish/formats/json/backends/base.py,sha256=WqtyoM82pyM0NyqpPwndrebr1bUVU1QlpmVQNrcAO8c,1114
193
193
  omlish/formats/json/backends/default.py,sha256=a-eM-Y1IHdpYvZFjazwq_orRncjtYR7BKxP_2kpEXog,322
@@ -197,13 +197,13 @@ omlish/formats/json/backends/std.py,sha256=PM00Kh9ZR2XzollHMEvdo35Eml1N-zFfRW-LO
197
197
  omlish/formats/json/backends/ujson.py,sha256=BNJCU4kluGHdqTUKLJEuHhE2m2TmqR7HEN289S0Eokg,2278
198
198
  omlish/formats/json/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
199
199
  omlish/formats/json/cli/__main__.py,sha256=1wxxKZVkj_u7HCcewwMIbGuZj_Wph95yrUbm474Op9M,188
200
- omlish/formats/json/cli/cli.py,sha256=GkdNokklRuDWiXAIai1wijSBFVJlpdlNLTQ3Lyucos0,5493
200
+ omlish/formats/json/cli/cli.py,sha256=a_Ak3x8U1ulK2EkG4DQ4XbfsV_4gz5YAigv43Ah_XlY,5491
201
201
  omlish/formats/json/cli/formats.py,sha256=tqEZKby4HeafGcaUs-m8B-2ZV12dRo40rzL-V99cp00,1714
202
202
  omlish/formats/json/stream/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
203
203
  omlish/formats/json/stream/build.py,sha256=6deXBxTx1toFAPShz2Jo5OiXxH5Y4ppG8gDPRFoUgjA,2461
204
- omlish/formats/json/stream/lex.py,sha256=01K_M9na7-3xv0h3VDBobLXuTS-w4YesNJ6GhOaDtYA,6494
204
+ omlish/formats/json/stream/lex.py,sha256=_JYoWFRpo3_dQmT7xXCWGFNoBT4M8m97G9oGzwPZeo4,6483
205
205
  omlish/formats/json/stream/parse.py,sha256=WkbW7tvcdrTSluKhw70nPvjsq943eryVcjx8FSz78tM,5198
206
- omlish/formats/json/stream/render.py,sha256=B9ZNuBiDJOT25prhIsZu1ICKjxk4eMPwpgQF37NPufs,3212
206
+ omlish/formats/json/stream/render.py,sha256=EKQ9tstWgnVdSlzJWNpX3diaSZWzVGLgIzRaSZMVqKM,3549
207
207
  omlish/graphs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
208
208
  omlish/graphs/dags.py,sha256=zp55lYgUdRCxmADwiGDHeehMJczZFA_tzdWqy77icOk,3047
209
209
  omlish/graphs/domination.py,sha256=oCGoWzWTxLwow0LDyGjjEf2AjFiOiDz4WaBtczwSbsQ,7576
@@ -396,8 +396,10 @@ omlish/specs/openapi/__init__.py,sha256=zilQhafjvteRDF_TUIRgF293dBC6g-TJChmUb6T9
396
396
  omlish/specs/openapi/marshal.py,sha256=ob_qUbT9-de86KhPjFccl_NP0liQcXK7Ao-b5Hn0VQA,2133
397
397
  omlish/specs/openapi/openapi.py,sha256=y4h04jeB7ORJSVrcy7apaBdpwLjIyscv1Ub5SderH2c,12682
398
398
  omlish/sql/__init__.py,sha256=TpZLsEJKJzvJ0eMzuV8hwOJJbkxBCV1RZPUMLAVB6io,173
399
- omlish/sql/_abc.py,sha256=HLhnnLZ7l0r_N99I-RCqJe6VHth-9iluU7cR-7-5jfs,1519
399
+ omlish/sql/_abc.py,sha256=kiOitW_ZhTXrJanJ582wD7o9K69v6HXqDPkxuHEAxrc,1606
400
+ omlish/sql/dbapi.py,sha256=5ghJH-HexsmDlYdWlhf00nCGQX2IC98_gxIxMkucOas,3195
400
401
  omlish/sql/dbs.py,sha256=lpdFmm2vTwLoBiVYGj9yPsVcTEYYNCxlYZZpjfChzkY,1870
402
+ omlish/sql/params.py,sha256=Z4VPet6GhNqD1T_MXSWSHkdy3cpUEhST-OplC4B_fYI,4433
401
403
  omlish/sql/qualifiedname.py,sha256=rlW3gVmyucJbqwcxj_7BfK4X2HoXrMroZT2H45zPgJQ,2264
402
404
  omlish/sql/alchemy/__init__.py,sha256=1ruDMiviH5fjevn2xVki-QspcE9O3VPy4hxOqpHjI2s,224
403
405
  omlish/sql/alchemy/asyncs.py,sha256=C1bIzz9m2Qbgl4qYGQt_FRQg4BKRcxUqMsVg9RtnTPg,3689
@@ -411,14 +413,14 @@ omlish/sql/queries/binary.py,sha256=dcEzeEn104AMPuQ7QrJU2O-YCN3SUdxB5S4jaWKOUqY,
411
413
  omlish/sql/queries/building.py,sha256=dIQyEqNef2egKAf5qO_aZvXxlAEfxIGWS23qvJ-ozqQ,591
412
414
  omlish/sql/queries/exprs.py,sha256=kAI5PmvfJ-TqEzzc9H4_womFShT1eA0jWSZH2j2Wg-c,1802
413
415
  omlish/sql/queries/idents.py,sha256=erW6fE9UapuvW1ZeLfGFz7yuW6zzktWIWmOuAHeF8_g,496
414
- omlish/sql/queries/inserts.py,sha256=PS3oqGjDgnjUd4sxHVpfKDQnsjG4_6KTseRzWyiJQl0,1229
415
- omlish/sql/queries/marshal.py,sha256=Q-N6RcBzpj3FIa2-7eX6dniLjXKzUhRC9g--61UQUbM,2890
416
+ omlish/sql/queries/inserts.py,sha256=Uo0ewnLPEt43dnX5AJWOC9ioViou7xYes7sGT9lD_0k,1281
417
+ omlish/sql/queries/marshal.py,sha256=uLN_gOqiYHBQioZEvg7OREycY0bzKwzBw5mnbg_0cio,2938
416
418
  omlish/sql/queries/multi.py,sha256=7x6x-4jnPzxA6ZasBjnjFuhHFpWt5rGCua3UvuTMIJ0,837
417
419
  omlish/sql/queries/names.py,sha256=YiIyS6ehYMYrdLlUxMawV_Xf2zdi7RwVO9Qsxr_W4_4,772
418
420
  omlish/sql/queries/ops.py,sha256=B7IDfjr2DW5LJhWoNaY1WW90BJhe5ZtmxIELhWXbW-0,129
419
421
  omlish/sql/queries/relations.py,sha256=-d3n-dN17c3TPMZmzSplhWawllUzdq12XPDAuzoeMEQ,838
420
- omlish/sql/queries/rendering.py,sha256=vpZY6rAjX5MZl72Tk6_tle3CUPi6U28sN4PcQ7aZ1iI,5055
421
- omlish/sql/queries/selects.py,sha256=EcHlyKl5kGSY1d3GVxnImhGCTB6WvwQnlSA9eZanBqU,1364
422
+ omlish/sql/queries/rendering.py,sha256=QegTkbSGlGKen4U4XB2lqjpkt1WGaZUmXmVR2gm8UiU,5944
423
+ omlish/sql/queries/selects.py,sha256=8dqm4KyrJvOsW3KHSO7qLEfPlZa7sJDp8puuGxrfvoA,1369
422
424
  omlish/sql/queries/stmts.py,sha256=pBqwD7dRlqMu6uh6vR3xaWOEgbZCcFWbOQ9ryYd17T4,441
423
425
  omlish/sql/queries/unary.py,sha256=MEYBDZn_H0bexmUrJeONOv5-gIpYowUaXOsEHeQM4ks,1144
424
426
  omlish/sql/tabledefs/__init__.py,sha256=TvtQsp-jJu6_ZahyCOFAaElSSBcRftNyJpdiDPGYCDk,190
@@ -454,9 +456,9 @@ omlish/text/delimit.py,sha256=ubPXcXQmtbOVrUsNh5gH1mDq5H-n1y2R4cPL5_DQf68,4928
454
456
  omlish/text/glyphsplit.py,sha256=Ug-dPRO7x-OrNNr8g1y6DotSZ2KH0S-VcOmUobwa4B0,3296
455
457
  omlish/text/indent.py,sha256=6Jj6TFY9unaPa4xPzrnZemJ-fHsV53IamP93XGjSUHs,1274
456
458
  omlish/text/parts.py,sha256=7vPF1aTZdvLVYJ4EwBZVzRSy8XB3YqPd7JwEnNGGAOo,6495
457
- omlish-0.0.0.dev86.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
458
- omlish-0.0.0.dev86.dist-info/METADATA,sha256=w6dXN4XU0iMfkJxV1fEHQ96o_V5idhf7cy3GApDwdAc,3987
459
- omlish-0.0.0.dev86.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
460
- omlish-0.0.0.dev86.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
461
- omlish-0.0.0.dev86.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
462
- omlish-0.0.0.dev86.dist-info/RECORD,,
459
+ omlish-0.0.0.dev88.dist-info/LICENSE,sha256=B_hVtavaA8zCYDW99DYdcpDLKz1n3BBRjZrcbv8uG8c,1451
460
+ omlish-0.0.0.dev88.dist-info/METADATA,sha256=_sR0eXqLj64VXjpIND0FJ_jFD56oVh9f7QXetS_XP0g,3987
461
+ omlish-0.0.0.dev88.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
462
+ omlish-0.0.0.dev88.dist-info/entry_points.txt,sha256=Lt84WvRZJskWCAS7xnQGZIeVWksprtUHj0llrvVmod8,35
463
+ omlish-0.0.0.dev88.dist-info/top_level.txt,sha256=pePsKdLu7DvtUiecdYXJ78iO80uDNmBlqe-8hOzOmfs,7
464
+ omlish-0.0.0.dev88.dist-info/RECORD,,