Skip to content

Commit 15e37ad

Browse files
committed
WIP: Oracle named params.
1 parent 914f305 commit 15e37ad

File tree

9 files changed

+607
-75
lines changed

9 files changed

+607
-75
lines changed

CHANGES.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ Change History
33
==============
44

55

6+
6.1.0 (TBD)
7+
------------------
8+
9+
New features:
10+
11+
- `Issue #11`_/`Pull #12`_: Support Oracle named parameters enclosed in double quotes (style `named_oracle`).
12+
13+
14+
.. _`Issue #11`: https://github.com/cpburnz/python-sqlparams/issues/11
15+
.. _`Pull #12`: https://github.com/cpburnz/python-sqlparams/pull/12
16+
17+
618
6.0.1 (2023-12-09)
719
------------------
820

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ classifiers = [
1818
"Programming Language :: Python :: 3.10",
1919
"Programming Language :: Python :: 3.11",
2020
"Programming Language :: Python :: 3.12",
21+
"Programming Language :: Python :: 3.13",
2122
"Programming Language :: Python :: Implementation :: CPython",
2223
"Programming Language :: Python :: Implementation :: PyPy",
2324
"Topic :: Database",

sqlparams/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def __init__(
6666
escape_char: Union[str, bool, None] = None,
6767
expand_tuples: Optional[bool] = None,
6868
strip_comments: Union[Sequence[Union[str, Tuple[str, str]]], bool, None] = None,
69-
allow_out_quotes: bool = False,
69+
allow_out_quotes: Optional[bool] = None,
7070
) -> None:
7171
"""
7272
Instantiates the :class:`.SQLParams` instance.
@@ -90,7 +90,7 @@ def __init__(
9090
*out_style* is a named style, do not expand tuples by default (``False``).
9191
9292
.. NOTE:: Empty tuples will be safely expanded to ``(NULL)`` to prevent SQL
93-
syntax errors,
93+
syntax errors.
9494
9595
*strip_comments* (:class:`~collections.abc.Sequence`, :class:`bool`, or
9696
``None``) whether to strip out comments and what style of comments to
@@ -104,6 +104,9 @@ def __init__(
104104
``DEFAULT_COMMENTS`` will be used (``"--"`` and ``("/*", "*/")`` styles).
105105
Default is ``None`` to not remove comments.
106106
107+
*allow_out_quotes* (:class:`bool` or ``None``) is whether to quote the out
108+
parameters when *out_style* supports it. Default is ``None`` for ``False``.
109+
107110
The following parameter styles are supported by both *in_style* and
108111
*out_style*:
109112
@@ -213,7 +216,7 @@ def __init__(
213216
in_style=in_style,
214217
out_obj=out_obj,
215218
out_style=out_style,
216-
allow_out_quotes=allow_out_quotes,
219+
allow_out_quotes=bool(allow_out_quotes),
217220
)
218221
"""
219222
*__converter* (:class:`._converting.Converter`) is the parameter converter

sqlparams/_converting.py

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,15 @@ def __init__(
7373
string.
7474
"""
7575

76-
self._out_style: _styles.Style = out_style
76+
self._out_quotes: bool = out_style.param_quotes and allow_out_quotes
7777
"""
78-
*_out_style* (:class:`._styles.Style`) is the out-style to use.
78+
*_out_quotes* (:class:`bool`) whether enclosing out parameters in double
79+
quotes.
7980
"""
8081

81-
self._out_quotes: bool = out_style.param_quotes and allow_out_quotes
82+
self._out_style: _styles.Style = out_style
8283
"""
83-
*_out_quotes* (:class:`bool`) whether enclosing out parameters in double quotes.
84+
*_out_style* (:class:`._styles.Style`) is the out-style to use.
8485
"""
8586

8687
def convert(
@@ -287,14 +288,6 @@ def __convert_params(
287288

288289
return out_params
289290

290-
@staticmethod
291-
def __unquote_param(param: str) -> str:
292-
if len(param) > 1 and param[0] == param[-1] == '"':
293-
return param[1:-1]
294-
if len(param) > 0 and (param[0] == '"' or param[-1] == '"'):
295-
raise KeyError(param)
296-
return param.upper()
297-
298291
def __regex_replace(
299292
self,
300293
in_params: Dict[str, Any],
@@ -333,7 +326,7 @@ def __regex_replace(
333326
if self._in_style.param_quotes:
334327
in_name = in_name_sql if quote else in_name_sql.upper()
335328
for in_name_param, in_value in in_params.items():
336-
if NamedToNamedConverter.__unquote_param(in_name_param) == in_name:
329+
if _unquote_oracle_param(in_name_param) == in_name:
337330
value = in_value
338331
break
339332
else:
@@ -351,18 +344,23 @@ def __regex_replace(
351344
out_names = []
352345
out_replacements = []
353346
for i, sub_value in enumerate(value):
354-
out_name = "{}__{}_sqlp".format(in_name, i)
355-
out_name = f'"{out_name}"' if self._out_quotes else out_name
347+
out_name = f"{in_name}__{i}_sqlp"
348+
if self._out_quotes:
349+
out_name = _quote_oracle_param(out_name)
350+
356351
out_repl = self._out_format.format(param=out_name)
357352
out_names.append(out_name)
358353
out_replacements.append(out_repl)
359354

360-
param_conversions.append((True, in_name, out_names))
355+
param_conversions.append((True, in_name_param, out_names))
361356
return "({})".format(",".join(out_replacements))
362357

363358
else:
364359
# Convert named parameter.
365-
out_name = f'"{in_name}"' if self._out_quotes else in_name
360+
out_name = in_name
361+
if self._out_quotes:
362+
out_name = _quote_oracle_param(out_name)
363+
366364
out_repl = self._out_format.format(param=out_name)
367365
param_conversions.append((False, in_name_param, out_name))
368366
return out_repl
@@ -586,9 +584,20 @@ def __regex_replace(
586584

587585
else:
588586
# Named parameter matched, return numeric out-style parameter.
589-
in_name = result['param']
587+
in_name_sql = result['param']
588+
quote = result.get("quote")
589+
if self._in_style.param_quotes:
590+
in_name = in_name_sql if quote else in_name_sql.upper()
591+
for in_name_param, in_value in in_params.items():
592+
if _unquote_oracle_param(in_name_param) == in_name:
593+
value = in_value
594+
break
595+
else:
596+
raise KeyError(in_name_sql)
597+
else:
598+
in_name_param = in_name = in_name_sql
599+
value = in_params[in_name]
590600

591-
value = in_params[in_name]
592601
if self._expand_tuples and isinstance(value, tuple):
593602
if not value:
594603
# Safely expand an empty tuple.
@@ -615,7 +624,7 @@ def __regex_replace(
615624
out_replacements.append(out_repl)
616625

617626
if is_new:
618-
param_conversions.append((True, in_name, out_indices))
627+
param_conversions.append((True, in_name_param, out_indices))
619628

620629
return "({})".format(",".join(out_replacements))
621630

@@ -631,7 +640,7 @@ def __regex_replace(
631640
out_num = out_index + self.__out_start
632641
out_repl = self._out_format.format(param=out_num)
633642
out_lookup[in_name] = (out_index, out_repl)
634-
param_conversions.append((False, in_name, out_index))
643+
param_conversions.append((False, in_name_param, out_index))
635644

636645
return out_repl
637646

@@ -826,22 +835,33 @@ def __regex_replace(
826835
return escape[self._escape_start:]
827836

828837
else:
829-
# Named parameter matched, return ordinal out-style parameter.
830-
in_name = result['param']
838+
# Named parameter matched, return numeric out-style parameter.
839+
in_name_sql = result['param']
840+
quote = result.get("quote")
841+
if self._in_style.param_quotes:
842+
in_name = in_name_sql if quote else in_name_sql.upper()
843+
for in_name_param, in_value in in_params.items():
844+
if _unquote_oracle_param(in_name_param) == in_name:
845+
value = in_value
846+
break
847+
else:
848+
raise KeyError(in_name_sql)
849+
else:
850+
in_name_param = in_name = in_name_sql
851+
value = in_params[in_name]
831852

832-
value = in_params[in_name]
833853
if self._expand_tuples and isinstance(value, tuple):
834854
if not value:
835855
# Safely expand an empty tuple.
836856
return "(NULL)"
837857

838858
# Convert named parameter by flattening tuple values.
839-
param_conversions.append((True, in_name, len(value)))
859+
param_conversions.append((True, in_name_param, len(value)))
840860
return "({})".format(",".join(out_format for _ in value))
841861

842862
else:
843863
# Convert named parameter.
844-
param_conversions.append((False, in_name, None))
864+
param_conversions.append((False, in_name_param, None))
845865
return out_format
846866

847867

@@ -2366,3 +2386,30 @@ def __regex_replace(
23662386
# Convert ordinal parameter.
23672387
param_conversions.append((False, in_index, None))
23682388
return out_format
2389+
2390+
2391+
def _quote_oracle_param(param: str) -> str:
2392+
"""
2393+
Quote the Oracle parameter.
2394+
2395+
*param* (:class:`str`) is the out-parameter.
2396+
2397+
Returns the quoted parameter (:class:`str`).
2398+
"""
2399+
out_name = param.replace('"', '""')
2400+
return f'"{out_name}"'
2401+
2402+
2403+
def _unquote_oracle_param(param: str) -> str:
2404+
"""
2405+
Unquote the Oracle parameter.
2406+
2407+
*param* (:class:`str`) is the in-parameter.
2408+
2409+
Returns the unquoted parameter (:class:`str`).
2410+
"""
2411+
if len(param) > 1 and param[0] == param[-1] == '"':
2412+
return param[1:-1]
2413+
elif len(param) > 0 and (param[0] == '"' or param[-1] == '"'):
2414+
raise KeyError(param)
2415+
return param.upper()

sqlparams/_styles.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def __init__(
2323
escape_regex: str,
2424
out_format: str,
2525
param_regex: str,
26-
param_quotes: bool = False
26+
param_quotes: bool = False,
2727
) -> None:
2828
"""
2929
Initializes the :class:`.Style` instances.
@@ -51,15 +51,16 @@ def __init__(
5151
*out_format* (:class:`str`) is the out-style parameter format string.
5252
"""
5353

54-
self.param_regex: str = param_regex
54+
self.param_quotes: bool = param_quotes
5555
"""
56-
*param_regex* (:class:`str`) is the regular expression used to extract the
57-
parameter.
56+
*param_quotes* (:class:`bool`) is whether the style supports enclosing
57+
parameters in quotes.
5858
"""
5959

60-
self.param_quotes: bool = param_quotes
60+
self.param_regex: str = param_regex
6161
"""
62-
*param_quotes* (:class:`bool`) whether style supports enclosing parameters in quotes.
62+
*param_regex* (:class:`str`) is the regular expression used to extract the
63+
parameter.
6364
"""
6465

6566

@@ -113,15 +114,6 @@ class OrdinalStyle(Style):
113114
out_format=":{param}"
114115
)
115116

116-
STYLES['named_oracle'] = NamedStyle(
117-
name="named_oracle",
118-
escape_char=":",
119-
escape_regex="(?P<escape>{char}:)",
120-
param_regex=r'(?<!:):(?P<quote>"?)(?P<param>[A-Za-z_]\w*)(?P=quote)',
121-
out_format=":{param}",
122-
param_quotes=True
123-
)
124-
125117
# Define non-standard "named_dollar" parameter style.
126118
STYLES['named_dollar'] = NamedStyle(
127119
name="named_dollar",
@@ -131,6 +123,16 @@ class OrdinalStyle(Style):
131123
out_format="${param}",
132124
)
133125

126+
# Define non-standard "named_oracle" parameter style.
127+
STYLES['named_oracle'] = NamedStyle(
128+
name="named_oracle",
129+
escape_char=":",
130+
escape_regex="(?P<escape>{char}:)",
131+
param_regex=r'(?<!:):(?P<quote>"?)(?P<param>[A-Za-z_]\w*)(?P=quote)',
132+
out_format=":{param}",
133+
param_quotes=True
134+
)
135+
134136
# Define standard "numeric" parameter style.
135137
STYLES['numeric'] = NumericStyle(
136138
name="numeric",

0 commit comments

Comments
 (0)