execsql2 2.22.8__py3-none-any.whl → 2.23.0__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.
- execsql/api.py +71 -0
- execsql/config.py +2 -0
- execsql/data/execsql.conf.template +6 -0
- execsql/db/access.py +12 -4
- execsql/db/base.py +15 -1
- execsql/db/dsn.py +12 -7
- execsql/db/duckdb.py +7 -8
- execsql/db/firebird.py +12 -7
- execsql/db/mysql.py +44 -10
- execsql/db/oracle.py +12 -7
- execsql/db/postgres.py +17 -9
- execsql/db/sqlite.py +7 -8
- execsql/db/sqlserver.py +7 -7
- execsql/db/tiers.py +79 -0
- execsql/exporters/delimited.py +15 -1
- execsql/exporters/html.py +8 -2
- execsql/exporters/latex.py +35 -2
- execsql/exporters/markdown.py +9 -3
- execsql/exporters/ods.py +22 -4
- execsql/exporters/pretty.py +13 -0
- execsql/exporters/templates.py +18 -1
- execsql/exporters/values.py +25 -4
- execsql/exporters/xlsx.py +7 -1
- execsql/state.py +5 -0
- execsql/state.pyi +3 -1
- execsql/types.py +4 -1
- execsql/utils/fileio.py +47 -2
- execsql/utils/mail.py +9 -5
- {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/METADATA +22 -13
- {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/RECORD +48 -47
- {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/WHEEL +1 -1
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.22.8.data → execsql2-2.23.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.22.8.dist-info → execsql2-2.23.0.dist-info}/licenses/NOTICE +0 -0
execsql/exporters/html.py
CHANGED
|
@@ -51,7 +51,10 @@ def export_html(
|
|
|
51
51
|
for r in rows:
|
|
52
52
|
f.write("<tr>")
|
|
53
53
|
for v in r:
|
|
54
|
-
|
|
54
|
+
# `if v is not None`, not `if v`: 0, False, and an empty
|
|
55
|
+
# string are all falsy, and a zero measurement silently became
|
|
56
|
+
# an empty cell.
|
|
57
|
+
f.write(f"<td>{html_mod.escape(str(v)) if v is not None else ''}</td>")
|
|
55
58
|
f.write("</tr>\n")
|
|
56
59
|
f.write("</tbody>\n</table>\n")
|
|
57
60
|
|
|
@@ -191,7 +194,10 @@ def export_cgi_html(
|
|
|
191
194
|
for r in rows:
|
|
192
195
|
f.write("<tr>")
|
|
193
196
|
for v in r:
|
|
194
|
-
|
|
197
|
+
# `if v is not None`, not `if v`: 0, False, and an empty
|
|
198
|
+
# string are all falsy, and a zero measurement silently became
|
|
199
|
+
# an empty cell.
|
|
200
|
+
f.write(f"<td>{html_mod.escape(str(v)) if v is not None else ''}</td>")
|
|
195
201
|
f.write("</tr>\n")
|
|
196
202
|
f.write("</tbody>\n</table>\n")
|
|
197
203
|
|
execsql/exporters/latex.py
CHANGED
|
@@ -20,6 +20,39 @@ import execsql.state as _state
|
|
|
20
20
|
__all__ = ["export_latex", "write_query_to_latex"]
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
#: Every character LaTeX treats specially, with its text-mode replacement.
|
|
24
|
+
#: ``&`` matters most here: it is the column separator this exporter writes, so
|
|
25
|
+
#: an ampersand in a value silently added a column and the .tex file no longer
|
|
26
|
+
#: compiled. ``%`` is nearly as bad — it comments out the rest of the line.
|
|
27
|
+
#: str.translate applies these simultaneously, so the backslash replacement
|
|
28
|
+
#: cannot be re-escaped by a later rule.
|
|
29
|
+
_LATEX_ESCAPE = str.maketrans(
|
|
30
|
+
{
|
|
31
|
+
"\\": r"\textbackslash{}",
|
|
32
|
+
"&": r"\&",
|
|
33
|
+
"%": r"\%",
|
|
34
|
+
"$": r"\$",
|
|
35
|
+
"#": r"\#",
|
|
36
|
+
"_": r"\_",
|
|
37
|
+
"{": r"\{",
|
|
38
|
+
"}": r"\}",
|
|
39
|
+
"~": r"\textasciitilde{}",
|
|
40
|
+
"^": r"\textasciicircum{}",
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _latex_cell(value: Any) -> str:
|
|
46
|
+
"""Render a value as LaTeX table-cell text.
|
|
47
|
+
|
|
48
|
+
``None`` becomes an empty cell rather than the literal string "None",
|
|
49
|
+
matching every other exporter.
|
|
50
|
+
"""
|
|
51
|
+
if value is None:
|
|
52
|
+
return ""
|
|
53
|
+
return str(value).translate(_LATEX_ESCAPE)
|
|
54
|
+
|
|
55
|
+
|
|
23
56
|
def export_latex(
|
|
24
57
|
outfile: str,
|
|
25
58
|
hdrs: list[str],
|
|
@@ -39,10 +72,10 @@ def export_latex(
|
|
|
39
72
|
f.write(f" \\caption{{{desc}}}\n")
|
|
40
73
|
f.write(f" \\begin{{tabular}} {{{' l' * len(hdrs)} }}\n")
|
|
41
74
|
f.write(" \\hline\n")
|
|
42
|
-
f.write(" " + " & ".join([h
|
|
75
|
+
f.write(" " + " & ".join([_latex_cell(h) for h in hdrs]) + " \\\\\n")
|
|
43
76
|
f.write(" \\hline\n")
|
|
44
77
|
for r in rows:
|
|
45
|
-
f.write(" " + " & ".join([
|
|
78
|
+
f.write(" " + " & ".join([_latex_cell(c) for c in r]) + " \\\\\n")
|
|
46
79
|
f.write(" \\hline\n")
|
|
47
80
|
f.write(" \\end{tabular}\n")
|
|
48
81
|
f.write(" \\end{table}\n")
|
execsql/exporters/markdown.py
CHANGED
|
@@ -35,15 +35,21 @@ def _cell(value: Any) -> str:
|
|
|
35
35
|
|
|
36
36
|
Args:
|
|
37
37
|
value: The cell value from the result set. ``None`` is rendered as
|
|
38
|
-
an empty string. Pipe characters are escaped
|
|
39
|
-
|
|
38
|
+
an empty string. Pipe characters are escaped, and newlines
|
|
39
|
+
become ``<br>``, so neither breaks the table structure.
|
|
40
40
|
|
|
41
41
|
Returns:
|
|
42
42
|
A string safe to embed between pipe characters in a GFM table row.
|
|
43
43
|
"""
|
|
44
44
|
if value is None:
|
|
45
45
|
return ""
|
|
46
|
-
|
|
46
|
+
text = str(value).translate(_PIPE_ESCAPE)
|
|
47
|
+
if "\n" in text or "\r" in text:
|
|
48
|
+
# A GFM table row is one line: a literal newline ends the row mid-cell
|
|
49
|
+
# and leaves the rest of the values outside the table entirely. <br> is
|
|
50
|
+
# how GFM expresses a line break inside a cell.
|
|
51
|
+
text = text.replace("\r\n", "\n").replace("\r", "\n").replace("\n", "<br>")
|
|
52
|
+
return text
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
def write_query_to_markdown(
|
execsql/exporters/ods.py
CHANGED
|
@@ -10,6 +10,7 @@ Provides :func:`write_query_to_ods` (single-sheet export),
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
13
|
+
import decimal
|
|
13
14
|
import getpass
|
|
14
15
|
import os
|
|
15
16
|
from pathlib import Path
|
|
@@ -239,7 +240,12 @@ class OdsFile:
|
|
|
239
240
|
if isinstance(item, bool):
|
|
240
241
|
# Booleans must be evaluated before numbers.
|
|
241
242
|
tc = of.table.TableCell(valuetype="boolean", value=1 if item else 0, stylename=style_name)
|
|
242
|
-
elif isinstance(item, float | int):
|
|
243
|
+
elif isinstance(item, float | int | decimal.Decimal):
|
|
244
|
+
# Decimal belongs here: PostgreSQL, MySQL and DuckDB all return it
|
|
245
|
+
# for numeric/decimal columns. Without it the value fell to the
|
|
246
|
+
# untyped branch below and the cell was written with an
|
|
247
|
+
# office:value but no office:value-type, which is not valid ODF —
|
|
248
|
+
# a reader is free to treat the number as text or ignore it.
|
|
243
249
|
tc = of.table.TableCell(valuetype="float", value=item, stylename=style_name)
|
|
244
250
|
elif isinstance(item, datetime.datetime):
|
|
245
251
|
self.define_iso_datetime_style()
|
|
@@ -266,12 +272,24 @@ class OdsFile:
|
|
|
266
272
|
tc = of.table.TableCell(timevalue=timeval.strftime("PT%HH%MM%S.%fS"), stylename="iso_datetime")
|
|
267
273
|
tc.addElement(of.text.P(text=timeval.strftime("%H:%M:%S.%f")))
|
|
268
274
|
elif isinstance(item, str):
|
|
269
|
-
|
|
270
|
-
|
|
275
|
+
# An XML parser normalises a newline inside an attribute to a
|
|
276
|
+
# space, so office:string-value cannot carry one. ODF's answer
|
|
277
|
+
# is one <text:p> per line, which is what LibreOffice writes;
|
|
278
|
+
# the attribute is set only for single-line values.
|
|
279
|
+
if "\n" in item or "\r" in item:
|
|
280
|
+
tc = of.table.TableCell(valuetype="string", stylename=style_name)
|
|
281
|
+
else:
|
|
282
|
+
tc = of.table.TableCell(valuetype="string", stringvalue=item, stylename=style_name)
|
|
271
283
|
else:
|
|
272
284
|
tc = of.table.TableCell(value=item, stylename=style_name)
|
|
273
285
|
if item is not None:
|
|
274
|
-
|
|
286
|
+
if isinstance(item, str) and ("\n" in item or "\r" in item):
|
|
287
|
+
# Previously these were flattened to spaces, silently changing
|
|
288
|
+
# the exported value.
|
|
289
|
+
for line in item.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
|
|
290
|
+
tc.addElement(of.text.P(text=line))
|
|
291
|
+
else:
|
|
292
|
+
tc.addElement(of.text.P(text=item))
|
|
275
293
|
tr.addElement(tc)
|
|
276
294
|
|
|
277
295
|
def add_sheet(self, of_table: Any) -> None:
|
execsql/exporters/pretty.py
CHANGED
|
@@ -36,6 +36,19 @@ def prettyprint_rowset(
|
|
|
36
36
|
if s is None:
|
|
37
37
|
return and_val
|
|
38
38
|
if isinstance(s, str):
|
|
39
|
+
# A newline or tab inside a value destroys a fixed-width table: the
|
|
40
|
+
# row is split across physical lines and every column after it is
|
|
41
|
+
# misaligned. This is a display format, so they are shown as their
|
|
42
|
+
# two-character escapes — the row stays one line, the columns line
|
|
43
|
+
# up, and the reader can still see that a break is there.
|
|
44
|
+
if "\n" in s or "\r" in s or "\t" in s:
|
|
45
|
+
return (
|
|
46
|
+
s.replace("\\", "\\\\")
|
|
47
|
+
.replace("\r\n", "\\n")
|
|
48
|
+
.replace("\r", "\\r")
|
|
49
|
+
.replace("\n", "\\n")
|
|
50
|
+
.replace("\t", "\\t")
|
|
51
|
+
)
|
|
39
52
|
return s
|
|
40
53
|
if type(s) in (type(memoryview(b"")), bytes, bytearray):
|
|
41
54
|
return f"Binary data ({len(s)} bytes)"
|
execsql/exporters/templates.py
CHANGED
|
@@ -105,7 +105,24 @@ class JinjaTemplateReport:
|
|
|
105
105
|
inf = EncodedFile(template_file, conf.script_encoding)
|
|
106
106
|
fh = inf.open("r")
|
|
107
107
|
try:
|
|
108
|
-
|
|
108
|
+
# Autoescape by template extension. The sandbox guards against a
|
|
109
|
+
# hostile *template*; nothing guarded against hostile *data*, so a
|
|
110
|
+
# value from the database rendered into an .html report as live
|
|
111
|
+
# markup — a `<script>` in a text column became a script tag.
|
|
112
|
+
# Keying on the extension leaves every non-markup template (csv,
|
|
113
|
+
# tex, txt, sql) byte-identical; only html/htm/xml change, and a
|
|
114
|
+
# template that genuinely wants raw markup asks for it with the
|
|
115
|
+
# standard `{{ value|safe }}`.
|
|
116
|
+
autoescape = self._jinja2.select_autoescape(
|
|
117
|
+
enabled_extensions=("html", "htm", "xml"),
|
|
118
|
+
default_for_string=False,
|
|
119
|
+
default=False,
|
|
120
|
+
)
|
|
121
|
+
env = SandboxedEnvironment(autoescape=autoescape)
|
|
122
|
+
# from_string() has no filename to inspect, so resolve the policy
|
|
123
|
+
# against the template's own path and pass the decision through.
|
|
124
|
+
env.autoescape = autoescape(template_file)
|
|
125
|
+
self.template = env.from_string(fh.read())
|
|
109
126
|
finally:
|
|
110
127
|
fh.close()
|
|
111
128
|
|
execsql/exporters/values.py
CHANGED
|
@@ -8,6 +8,7 @@ set as a series of SQL ``INSERT INTO … VALUES (…)`` statements, suitable
|
|
|
8
8
|
for loading data into a database from a plain SQL file.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import decimal
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
13
14
|
import execsql.state as _state
|
|
@@ -19,6 +20,29 @@ from execsql.utils.fileio import filewriter_close
|
|
|
19
20
|
__all__ = ["export_values", "write_query_to_values"]
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
def _sql_literal(value: Any) -> str:
|
|
24
|
+
"""Render a value as a SQL literal for an INSERT ... VALUES list.
|
|
25
|
+
|
|
26
|
+
Only numbers are emitted bare. Everything else is quoted, with embedded
|
|
27
|
+
single quotes doubled — the SQL standard escape.
|
|
28
|
+
|
|
29
|
+
Quoting is the safe default rather than the exception. Previously anything
|
|
30
|
+
that was not a string went out through ``str()`` unquoted, so a date became
|
|
31
|
+
``2026-09-24``, which SQL reads as arithmetic: PostgreSQL inserted 1993 and
|
|
32
|
+
reported no error at all. A timestamp, carrying a space, was a syntax
|
|
33
|
+
error instead. Any type this function has not been taught about is safer
|
|
34
|
+
quoted than bare.
|
|
35
|
+
"""
|
|
36
|
+
if value is None:
|
|
37
|
+
return "NULL"
|
|
38
|
+
if isinstance(value, bool):
|
|
39
|
+
# Checked before int — bool is a subclass of it.
|
|
40
|
+
return "TRUE" if value else "FALSE"
|
|
41
|
+
if isinstance(value, int | float | decimal.Decimal):
|
|
42
|
+
return str(value)
|
|
43
|
+
return "'" + str(value).replace("'", "''") + "'"
|
|
44
|
+
|
|
45
|
+
|
|
22
46
|
def export_values(
|
|
23
47
|
outfile: str,
|
|
24
48
|
hdrs: list[str],
|
|
@@ -54,10 +78,7 @@ def export_values(
|
|
|
54
78
|
firstrow = False
|
|
55
79
|
else:
|
|
56
80
|
f.write(",\n")
|
|
57
|
-
quoted_row = [
|
|
58
|
-
f"'{v.replace(chr(39), chr(39) * 2)}'" if isinstance(v, str) else str(v) if v is not None else "NULL"
|
|
59
|
-
for v in r
|
|
60
|
-
]
|
|
81
|
+
quoted_row = [_sql_literal(v) for v in r]
|
|
61
82
|
f.write(f" ({', '.join(quoted_row)})")
|
|
62
83
|
f.write("\n ;\n")
|
|
63
84
|
finally:
|
execsql/exporters/xlsx.py
CHANGED
|
@@ -9,6 +9,7 @@ Provides :func:`write_query_to_xlsx` (single-sheet export) and
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import datetime
|
|
12
|
+
import decimal
|
|
12
13
|
import getpass
|
|
13
14
|
import os
|
|
14
15
|
from pathlib import Path
|
|
@@ -52,7 +53,12 @@ def _cell_value(item: Any) -> Any:
|
|
|
52
53
|
if isinstance(item, bool):
|
|
53
54
|
# bool must be checked before int — bool is a subclass of int.
|
|
54
55
|
return item
|
|
55
|
-
if isinstance(item, int | float):
|
|
56
|
+
if isinstance(item, int | float | decimal.Decimal):
|
|
57
|
+
# Decimal is in openpyxl's NUMERIC_TYPES and becomes a real numeric
|
|
58
|
+
# cell. Without it, every ``numeric``/``decimal`` column — which is
|
|
59
|
+
# what PostgreSQL, MySQL and DuckDB return for measurements — fell
|
|
60
|
+
# through to str() below and arrived in Excel as text: no sums, no
|
|
61
|
+
# sorting, no charts.
|
|
56
62
|
return item
|
|
57
63
|
if isinstance(item, datetime.datetime):
|
|
58
64
|
return item
|
execsql/state.py
CHANGED
|
@@ -222,6 +222,7 @@ _CONTEXT_ATTRS: frozenset[str] = frozenset(
|
|
|
222
222
|
"status",
|
|
223
223
|
"output",
|
|
224
224
|
"filewriter",
|
|
225
|
+
"tier_notices_shown",
|
|
225
226
|
# Lazy singletons
|
|
226
227
|
"counters",
|
|
227
228
|
"timer",
|
|
@@ -275,6 +276,7 @@ class RuntimeContext:
|
|
|
275
276
|
"status",
|
|
276
277
|
"output",
|
|
277
278
|
"filewriter",
|
|
279
|
+
"tier_notices_shown",
|
|
278
280
|
# Lazy singletons
|
|
279
281
|
"counters",
|
|
280
282
|
"timer",
|
|
@@ -321,6 +323,9 @@ class RuntimeContext:
|
|
|
321
323
|
self.status: StatObj | None = None
|
|
322
324
|
self.output: WriteHooks | None = None
|
|
323
325
|
self.filewriter: FileWriter | None = None
|
|
326
|
+
# DBMS names whose best-effort support notice has already been shown,
|
|
327
|
+
# so repeated connections to the same backend announce it only once.
|
|
328
|
+
self.tier_notices_shown: set[str] = set()
|
|
324
329
|
|
|
325
330
|
# Lazy singletons
|
|
326
331
|
self.counters: CounterVars | None = None
|
execsql/state.pyi
CHANGED
|
@@ -51,7 +51,8 @@ exec_log: Logger
|
|
|
51
51
|
subvars: SubVarSet
|
|
52
52
|
status: StatObj
|
|
53
53
|
output: Any
|
|
54
|
-
|
|
54
|
+
tier_notices_shown: set[str]
|
|
55
|
+
filewriter: FileWriter | None
|
|
55
56
|
counters: CounterVars
|
|
56
57
|
timer: Timer
|
|
57
58
|
dbs: DatabasePool
|
|
@@ -84,6 +85,7 @@ class RuntimeContext:
|
|
|
84
85
|
subvars: SubVarSet | None
|
|
85
86
|
status: StatObj | None
|
|
86
87
|
output: Any
|
|
88
|
+
tier_notices_shown: set[str]
|
|
87
89
|
filewriter: FileWriter | None
|
|
88
90
|
counters: CounterVars | None
|
|
89
91
|
timer: Timer | None
|
execsql/types.py
CHANGED
|
@@ -773,7 +773,10 @@ dbt_duckdb.name_datatype(DT_Date, "DATE")
|
|
|
773
773
|
dbt_duckdb.name_datatype(DT_Time, "TIME")
|
|
774
774
|
dbt_duckdb.name_datatype(DT_Integer, "INTEGER")
|
|
775
775
|
dbt_duckdb.name_datatype(DT_Long, "BIGINT")
|
|
776
|
-
|
|
776
|
+
# DOUBLE, not REAL: DT_Float is IEEE double precision and a Python float
|
|
777
|
+
# always is one. DuckDB's REAL is single precision, so 1234.567 came back
|
|
778
|
+
# as 1234.5670166015625. Every other backend maps DT_Float to a double.
|
|
779
|
+
dbt_duckdb.name_datatype(DT_Float, "DOUBLE")
|
|
777
780
|
dbt_duckdb.name_datatype(DT_Decimal, "NUMERIC")
|
|
778
781
|
dbt_duckdb.name_datatype(DT_Boolean, "BOOLEAN")
|
|
779
782
|
dbt_duckdb.name_datatype(DT_Character, "TEXT")
|
execsql/utils/fileio.py
CHANGED
|
@@ -435,8 +435,40 @@ def filewriter_filestatus(filename: str) -> int:
|
|
|
435
435
|
return cast(int, fw_output.get())
|
|
436
436
|
|
|
437
437
|
|
|
438
|
+
#: Set once the first dropped write has been reported, so a script writing
|
|
439
|
+
#: thousands of lines warns once rather than per line.
|
|
440
|
+
_drop_warned = False
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _warn_write_dropped(filename: str) -> None:
|
|
444
|
+
"""Report, once per process, that file output is being discarded.
|
|
445
|
+
|
|
446
|
+
Silently dropping a write is not a safe default: a script's logfile simply
|
|
447
|
+
comes back empty and the run still reports success. The guard itself has
|
|
448
|
+
to stay — putting on a queue no subprocess is draining fills the OS pipe
|
|
449
|
+
buffer and deadlocks the caller — so the write is still dropped, but no
|
|
450
|
+
longer without saying so.
|
|
451
|
+
"""
|
|
452
|
+
global _drop_warned
|
|
453
|
+
if _drop_warned:
|
|
454
|
+
return
|
|
455
|
+
_drop_warned = True
|
|
456
|
+
try:
|
|
457
|
+
from execsql.utils.errors import write_warning
|
|
458
|
+
|
|
459
|
+
write_warning(
|
|
460
|
+
f'no file writer is running; output to "{filename}" and any later file '
|
|
461
|
+
"is discarded. Start one with execsql.utils.fileio.FileWriter, or run "
|
|
462
|
+
"the script through the CLI or execsql.api.run().",
|
|
463
|
+
always=True,
|
|
464
|
+
)
|
|
465
|
+
except Exception:
|
|
466
|
+
pass # Never let the warning path break the caller.
|
|
467
|
+
|
|
468
|
+
|
|
438
469
|
def filewriter_write(filename: str, message: str) -> None:
|
|
439
470
|
if not _writer_alive():
|
|
471
|
+
_warn_write_dropped(filename)
|
|
440
472
|
return
|
|
441
473
|
fw_input.put((FileWriter.CMD_WRITE, (filename, message)))
|
|
442
474
|
|
|
@@ -445,6 +477,7 @@ def filewriter_open_as_new(filename: str) -> None:
|
|
|
445
477
|
# FileWriter opens files in append mode ("a") by default. This ensures that it
|
|
446
478
|
# will be opened in write mode ("w") instead. If the file is open, it will be closed.
|
|
447
479
|
if not _writer_alive():
|
|
480
|
+
_warn_write_dropped(filename)
|
|
448
481
|
return
|
|
449
482
|
fw_input.put((FileWriter.CMD_OPEN_AS_NEW, (filename,)))
|
|
450
483
|
|
|
@@ -537,7 +570,19 @@ class EncodedFile:
|
|
|
537
570
|
self.encoding, self.bom_length = detect_by_bom(filename, file_encoding)
|
|
538
571
|
self.fo: io.TextIOWrapper | None = None
|
|
539
572
|
|
|
540
|
-
def open(self, mode: str = "r") -> io.TextIOWrapper:
|
|
573
|
+
def open(self, mode: str = "r", newline: str | None = None) -> io.TextIOWrapper:
|
|
574
|
+
"""Open the file in text mode.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
mode: Any mode accepted by :func:`open`.
|
|
578
|
+
newline: Passed straight to :func:`open`. The default ``None``
|
|
579
|
+
enables universal-newline translation, which rewrites every
|
|
580
|
+
``\n`` written as ``os.linesep`` — ``\r\n`` on Windows. That
|
|
581
|
+
is right for line terminators and wrong for data: a newline
|
|
582
|
+
*inside* a quoted field is rewritten too, so the value read
|
|
583
|
+
back is not the value exported. Writers whose output can
|
|
584
|
+
carry an embedded newline pass ``""`` to disable it.
|
|
585
|
+
"""
|
|
541
586
|
import execsql.state as _state
|
|
542
587
|
|
|
543
588
|
conf = _state.conf
|
|
@@ -548,7 +593,7 @@ class EncodedFile:
|
|
|
548
593
|
mode=mode,
|
|
549
594
|
encoding=self.encoding,
|
|
550
595
|
errors=conf.enc_err_disposition,
|
|
551
|
-
newline=
|
|
596
|
+
newline=newline,
|
|
552
597
|
),
|
|
553
598
|
)
|
|
554
599
|
assert self.fo is not None
|
execsql/utils/mail.py
CHANGED
|
@@ -66,15 +66,19 @@ class Mailer:
|
|
|
66
66
|
self.smtpconn = smtplib.SMTP_SSL(conf.smtp_host, conf.smtp_port, timeout=smtp_timeout)
|
|
67
67
|
else:
|
|
68
68
|
self.smtpconn = smtplib.SMTP(conf.smtp_host, conf.smtp_port, timeout=smtp_timeout)
|
|
69
|
-
self.smtpconn.
|
|
69
|
+
self.smtpconn.ehlo_or_helo_if_needed()
|
|
70
70
|
if conf.smtp_tls:
|
|
71
71
|
self.smtpconn.starttls()
|
|
72
72
|
self.smtpconn.ehlo(conf.smtp_host)
|
|
73
73
|
if conf.smtp_username:
|
|
74
|
-
if conf.smtp_password:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
if not conf.smtp_password:
|
|
75
|
+
# smtplib.login() requires a password; calling it with only a
|
|
76
|
+
# username raises TypeError deep in the stdlib.
|
|
77
|
+
raise ErrInfo(
|
|
78
|
+
type="error",
|
|
79
|
+
other_msg="Can't send email; an SMTP username is configured but no password.",
|
|
80
|
+
)
|
|
81
|
+
self.smtpconn.login(conf.smtp_username, conf.smtp_password)
|
|
78
82
|
|
|
79
83
|
def sendmail(
|
|
80
84
|
self,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.23.0
|
|
4
4
|
Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
|
|
5
5
|
Project-URL: Homepage, https://execsql2.readthedocs.io
|
|
6
6
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
@@ -224,17 +224,26 @@ execsql script.sql # read connection from config file
|
|
|
224
224
|
|
|
225
225
|
### Supported Databases
|
|
226
226
|
|
|
227
|
-
| Flag | Database |
|
|
228
|
-
| ---- | --------------- |
|
|
229
|
-
| `p` | PostgreSQL |
|
|
230
|
-
| `m` | MySQL / MariaDB |
|
|
231
|
-
| `s` | MS SQL Server |
|
|
232
|
-
| `l` | SQLite |
|
|
233
|
-
| `k` | DuckDB |
|
|
234
|
-
| `a` | MS Access |
|
|
235
|
-
| `f` | Firebird |
|
|
236
|
-
| `o` | Oracle |
|
|
237
|
-
| `d` | ODBC DSN |
|
|
227
|
+
| Flag | Database | Support |
|
|
228
|
+
| ---- | --------------- | ------------------------------------------------ |
|
|
229
|
+
| `p` | PostgreSQL | Supported — verified against a live server in CI |
|
|
230
|
+
| `m` | MySQL / MariaDB | Supported — verified against a live server in CI |
|
|
231
|
+
| `s` | MS SQL Server | Supported — verified against a live server in CI |
|
|
232
|
+
| `l` | SQLite | Supported — verified against real files in CI |
|
|
233
|
+
| `k` | DuckDB | Supported — verified against real files in CI |
|
|
234
|
+
| `a` | MS Access | Best effort — not verified in CI |
|
|
235
|
+
| `f` | Firebird | Best effort — not verified in CI |
|
|
236
|
+
| `o` | Oracle | Best effort — not verified in CI |
|
|
237
|
+
| `d` | ODBC DSN | Best effort — not verified in CI |
|
|
238
|
+
|
|
239
|
+
**Supported** adapters run against a live server or a real database file on
|
|
240
|
+
every CI run; a regression blocks the release and bugs get fixed. **Best
|
|
241
|
+
effort** adapters are carried forward from the upstream monolith and are not
|
|
242
|
+
exercised anywhere in CI — the code is there and may work, but nothing proves
|
|
243
|
+
it still does. Issues and pull requests are welcome for them; no guarantee is
|
|
244
|
+
made. Opening a best-effort connection prints one informational line, which
|
|
245
|
+
`support_tier_notice=No` in the `[interface]` section of `execsql.conf`
|
|
246
|
+
silences.
|
|
238
247
|
|
|
239
248
|
### Common options
|
|
240
249
|
|
|
@@ -384,7 +393,7 @@ execsql-format --no-sql --in-place scripts/
|
|
|
384
393
|
```yaml
|
|
385
394
|
repos:
|
|
386
395
|
- repo: https://github.com/geocoug/execsql
|
|
387
|
-
rev: v2.
|
|
396
|
+
rev: v2.23.0
|
|
388
397
|
hooks:
|
|
389
398
|
- id: execsql-format
|
|
390
399
|
```
|
|
@@ -1,56 +1,57 @@
|
|
|
1
1
|
execsql/__init__.py,sha256=ZeVnpEWu8M2bn3qLmVM72NhWRueUGNbsX8UAPTermo0,901
|
|
2
2
|
execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
|
|
3
|
-
execsql/api.py,sha256=
|
|
4
|
-
execsql/config.py,sha256=
|
|
3
|
+
execsql/api.py,sha256=gBNEINlQSncfhO1tvc-qD1MBXTuhDVIP16LhiqfnzQI,25334
|
|
4
|
+
execsql/config.py,sha256=riUh3DVOoScpqF-GK2EAcycu_1Y9PhWTghDuJSSUJjs,34507
|
|
5
5
|
execsql/exceptions.py,sha256=GlphAiR8iZu4UR4U50WgSqEUMd9_8AeQJ_hhPcJOeQU,9381
|
|
6
6
|
execsql/format.py,sha256=OOxsOX6g7otVgL3gI6uGxa5WIh8GTvjJ8WO-_P91zXg,52886
|
|
7
7
|
execsql/models.py,sha256=-DWTVrPNwM-n1h3e3rXMoBv9-wpWVdZahyy4RB7fRRE,15502
|
|
8
8
|
execsql/parser.py,sha256=aOxZ7Mzgvk7G4-nvy9Bgz7xlaZm6oo81cffDeBk_LCk,15597
|
|
9
9
|
execsql/plugins.py,sha256=l82VWa5NpvOQWD6vcO-WKP-vtn0wp9__wWDFAbwxZg0,12838
|
|
10
10
|
execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
-
execsql/state.py,sha256=
|
|
12
|
-
execsql/state.pyi,sha256=
|
|
13
|
-
execsql/types.py,sha256=
|
|
11
|
+
execsql/state.py,sha256=wu7T1fkXROiQ2LoBYLONYj-1BkDUdoYzV425aCeAwm4,22230
|
|
12
|
+
execsql/state.pyi,sha256=YByEn5N7r0M4vDceOoDGTgb2tRJ_gNkYWGjlLiqQorI,3839
|
|
13
|
+
execsql/types.py,sha256=8rAoRaWwgBnR-ZhNEfahTTbZ_D4fB4vBuGeMEUnu7vE,32454
|
|
14
14
|
execsql/cli/__init__.py,sha256=WPwEIELz-9XFG3Dpf-037eeYBdjtZStxWkT1bc3tMek,23112
|
|
15
15
|
execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
|
|
16
16
|
execsql/cli/help.py,sha256=ThwdZuMIfLPxLAPpGWwXFY_UfyWvYOCjdlBNK20Vzd8,5718
|
|
17
17
|
execsql/cli/lint.py,sha256=YqKzFNUhyb_Th69hYgKk1ZZVjCsZfJMIiUGqp06JwNs,17236
|
|
18
18
|
execsql/cli/run.py,sha256=Yb-Ri3cds0st1hUn4_w-PHMphTsArHhC4fx919SW9gU,39390
|
|
19
19
|
execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
execsql/data/execsql.conf.template,sha256=
|
|
20
|
+
execsql/data/execsql.conf.template,sha256=qqH_G3SiKBepljY3Go4rDDlhe3pIM6gOgsjAnLn0arg,11244
|
|
21
21
|
execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
|
|
22
|
-
execsql/db/access.py,sha256=
|
|
23
|
-
execsql/db/base.py,sha256=
|
|
24
|
-
execsql/db/dsn.py,sha256=
|
|
25
|
-
execsql/db/duckdb.py,sha256=
|
|
22
|
+
execsql/db/access.py,sha256=tcnWNqcCzpBtYKHIdfYj7xaJiaiDIzq0JUSIGf4KrrI,19823
|
|
23
|
+
execsql/db/base.py,sha256=oqRQXvf7kwCdgQMJQuJ_rKOI8UAnakaNCr_ivdC88BA,36102
|
|
24
|
+
execsql/db/dsn.py,sha256=nP7cESX7pJdZSPzyo2nYj6972GpPVVc8ESwyYT0Z_yQ,6523
|
|
25
|
+
execsql/db/duckdb.py,sha256=QQkGH8FVWEF9u5cM-Mtit-lBtmnWm4zo4BMMSiiEXdM,3176
|
|
26
26
|
execsql/db/factory.py,sha256=C_cyi3GjNx0opuwc6maFFeCSkjfOfoUjuf68YT4VAHE,5377
|
|
27
|
-
execsql/db/firebird.py,sha256=
|
|
28
|
-
execsql/db/mysql.py,sha256=
|
|
29
|
-
execsql/db/oracle.py,sha256=
|
|
30
|
-
execsql/db/postgres.py,sha256=
|
|
31
|
-
execsql/db/sqlite.py,sha256=
|
|
32
|
-
execsql/db/sqlserver.py,sha256=
|
|
27
|
+
execsql/db/firebird.py,sha256=fbzlGB6DeR-S9RFLUD4tJYPN-_76tyRLBkQl5LRJTDo,9640
|
|
28
|
+
execsql/db/mysql.py,sha256=Mb35hJOAviuf_QGJgIL17ftF-Xr2eb78z0cFVMRn_5E,19835
|
|
29
|
+
execsql/db/oracle.py,sha256=gBcSJaS92VV_LrxAPY8wFCqa4ouD8TiEhUB4dRK3la0,12174
|
|
30
|
+
execsql/db/postgres.py,sha256=HfrWuJ5-K5le1sY9iGv_E51JHciPIdA0UvyLsTKlnO4,22568
|
|
31
|
+
execsql/db/sqlite.py,sha256=4NRCO75DOYmkGrFlC6k_i8Hxd4jadLDbuRBYOI6CD8E,11498
|
|
32
|
+
execsql/db/sqlserver.py,sha256=O4E9OD3V3Xowsjl3KMdYHNCBeETlkJS9HUzNKEKuA9A,8943
|
|
33
|
+
execsql/db/tiers.py,sha256=PH2sUYwlYqcANGVKXprWQKVIlM6fpWU9q74djn8oFwc,2924
|
|
33
34
|
execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
|
|
34
35
|
execsql/debug/repl.py,sha256=JObeoEXh15qyk4Q3WQCCqMcBtojlIcu_Xg-ZRDZJi5Q,25491
|
|
35
36
|
execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
|
|
36
37
|
execsql/exporters/base.py,sha256=_GoO0YqwlTxrjogxHI3CxxRR64sMgIEjRogt0KCqzoY,6402
|
|
37
|
-
execsql/exporters/delimited.py,sha256=
|
|
38
|
+
execsql/exporters/delimited.py,sha256=v47Js8ZrQfTqx0aTDAD-9nkOx4ilAPnk_dZT3vAoCTw,33632
|
|
38
39
|
execsql/exporters/duckdb.py,sha256=912TMdYaSVD-kb-_oebTBHQp8FvYMbWTDEDD80w5Z50,3455
|
|
39
40
|
execsql/exporters/feather.py,sha256=tah48aep0cQRikZcmADXUPTzFddgDUT78Ndw_l0Hsj4,4178
|
|
40
|
-
execsql/exporters/html.py,sha256=
|
|
41
|
+
execsql/exporters/html.py,sha256=hwf9UHs7EZ7xiP3p9ilBIsAukXcDu-KO2dcQHVh2z30,10111
|
|
41
42
|
execsql/exporters/json.py,sha256=wWGT6zjvIdWlO5FmXbaEw1xOhy23f1YY6bm5uQD_NS0,4393
|
|
42
|
-
execsql/exporters/latex.py,sha256=
|
|
43
|
-
execsql/exporters/markdown.py,sha256=
|
|
44
|
-
execsql/exporters/ods.py,sha256=
|
|
43
|
+
execsql/exporters/latex.py,sha256=bdUN3GwfENUZHgpw6bvjsjfivDgzyf-HRkYTP-eH-pM,5480
|
|
44
|
+
execsql/exporters/markdown.py,sha256=FuMGFxm9uZUOrdEuXT2ZZLCqmVDly7SMrFlrjP6kVEI,4827
|
|
45
|
+
execsql/exporters/ods.py,sha256=Ke9pfcyT6R85LWIWcMI6pzRzkKir5w_gmiptCeeuoBc,20730
|
|
45
46
|
execsql/exporters/parquet.py,sha256=mbCo2Wlit1lkXmyo5EyLd_ATIHSICS49bouiBT2kdAs,1121
|
|
46
|
-
execsql/exporters/pretty.py,sha256=
|
|
47
|
+
execsql/exporters/pretty.py,sha256=SiJHBXIjoFsOHegXcdrtxYa6xSIPga1U0DQEy7pK4QY,4145
|
|
47
48
|
execsql/exporters/protocol.py,sha256=BxATgz0xKHbB2FpZBeNg7wZfIiCohhD1awlr3JCec0c,4372
|
|
48
49
|
execsql/exporters/raw.py,sha256=9o3BMt7UYmnjbP8OEzmM2kNKfwwERi2yDcuNfUoEfuQ,2530
|
|
49
50
|
execsql/exporters/sqlite.py,sha256=QjP_W90S-hecNqp2A-Vy58DD8MZitME1sTq4lqIS43w,3401
|
|
50
|
-
execsql/exporters/templates.py,sha256=
|
|
51
|
-
execsql/exporters/values.py,sha256=
|
|
51
|
+
execsql/exporters/templates.py,sha256=nRFurwTb5nxoyHpx609FGIp3MDbX_mYQ3IvgswaEbyA,7054
|
|
52
|
+
execsql/exporters/values.py,sha256=UPxN5IprasBZLqM5jfUXQihjdTeBFTxbJslOEtmGOcY,3384
|
|
52
53
|
execsql/exporters/xls.py,sha256=iZ_N7hneyfaDKbO1XdYv6doJCFPqJWEJql5rduoNyRU,11197
|
|
53
|
-
execsql/exporters/xlsx.py,sha256=
|
|
54
|
+
execsql/exporters/xlsx.py,sha256=5pTN8OsygLjk3MADJf8O7t3bdzSul7rrVO7SVSR581c,12223
|
|
54
55
|
execsql/exporters/xml.py,sha256=t1PSr3i8xvTCWzd9jaxxAy5NcRpl6sqjnZALafKCqpM,2575
|
|
55
56
|
execsql/exporters/yaml.py,sha256=R61Rs_0rd5G3bg344L4hC84GM79IfYXaDO2tkF-BluY,3121
|
|
56
57
|
execsql/exporters/zip.py,sha256=341z4Z4MPtEkXJEtfiMSiuJigERh5_f8RDbNMBbt8KM,5078
|
|
@@ -94,30 +95,30 @@ execsql/utils/auth.py,sha256=8Wixkx5nayEBG7RT4WTUQpCiElIKS3yOTV_6_-hHTRs,8889
|
|
|
94
95
|
execsql/utils/crypto.py,sha256=KlT6lPk-v0-qQQfAMfWvT9Q6kRdCD3_5JVaLajNocKM,3051
|
|
95
96
|
execsql/utils/datetime.py,sha256=8BrD24KWeMoouJZiFaXK4HF7plc-O_xrzIw12qOEyp8,3629
|
|
96
97
|
execsql/utils/errors.py,sha256=bDWooUmGdN8xqUt6csq466wPqKPLHHSi7U4hsmrFZxA,8577
|
|
97
|
-
execsql/utils/fileio.py,sha256=
|
|
98
|
+
execsql/utils/fileio.py,sha256=ONLoXkq5PV4dvNc16UMgi9-EY2Wy4mNUFxneSrX91N0,35231
|
|
98
99
|
execsql/utils/gui.py,sha256=X5aw6LVoeaAdHaQkymm4ba176jga0cS2GrqxSFWTWTg,23853
|
|
99
|
-
execsql/utils/mail.py,sha256=
|
|
100
|
+
execsql/utils/mail.py,sha256=SXZac24p62qxDbIviO8VdDZTbKljIoJWgWZfxIGu5zI,6260
|
|
100
101
|
execsql/utils/numeric.py,sha256=_7bi5R6pHjMCkOPne_Sh5nUj-B8e9g6WFmjptFHaunA,1573
|
|
101
102
|
execsql/utils/regex.py,sha256=B3mSBhRRx_DpUnX_t9IoYd05KvWAebiLMCZqQAmEwcw,3885
|
|
102
103
|
execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
|
|
103
104
|
execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
|
|
104
|
-
execsql2-2.
|
|
105
|
-
execsql2-2.
|
|
106
|
-
execsql2-2.
|
|
107
|
-
execsql2-2.
|
|
108
|
-
execsql2-2.
|
|
109
|
-
execsql2-2.
|
|
110
|
-
execsql2-2.
|
|
111
|
-
execsql2-2.
|
|
112
|
-
execsql2-2.
|
|
113
|
-
execsql2-2.
|
|
114
|
-
execsql2-2.
|
|
115
|
-
execsql2-2.
|
|
116
|
-
execsql2-2.
|
|
117
|
-
execsql2-2.
|
|
118
|
-
execsql2-2.
|
|
119
|
-
execsql2-2.
|
|
120
|
-
execsql2-2.
|
|
121
|
-
execsql2-2.
|
|
122
|
-
execsql2-2.
|
|
123
|
-
execsql2-2.
|
|
105
|
+
execsql2-2.23.0.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
|
|
106
|
+
execsql2-2.23.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
107
|
+
execsql2-2.23.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
|
|
108
|
+
execsql2-2.23.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
109
|
+
execsql2-2.23.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
|
|
110
|
+
execsql2-2.23.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
|
|
111
|
+
execsql2-2.23.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
|
|
112
|
+
execsql2-2.23.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
|
|
113
|
+
execsql2-2.23.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
|
|
114
|
+
execsql2-2.23.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
|
|
115
|
+
execsql2-2.23.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
|
|
116
|
+
execsql2-2.23.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
|
|
117
|
+
execsql2-2.23.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
|
|
118
|
+
execsql2-2.23.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
|
|
119
|
+
execsql2-2.23.0.dist-info/METADATA,sha256=i-ekOSO01zUc7UtJwXvGJLTZtmvA78sZSbsRXUO0E20,21504
|
|
120
|
+
execsql2-2.23.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
121
|
+
execsql2-2.23.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
122
|
+
execsql2-2.23.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
123
|
+
execsql2-2.23.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
|
|
124
|
+
execsql2-2.23.0.dist-info/RECORD,,
|
|
File without changes
|