execsql2 2.22.4__py3-none-any.whl → 2.22.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. execsql/format.py +291 -13
  2. {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/METADATA +2 -2
  3. {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/RECORD +21 -21
  4. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/README.md +0 -0
  5. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  6. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  7. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/make_config_db.sql +0 -0
  8. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/md_compare.sql +0 -0
  9. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/md_glossary.sql +0 -0
  10. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/md_upsert.sql +0 -0
  11. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/pg_compare.sql +0 -0
  12. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  13. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  14. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/script_template.sql +0 -0
  15. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/ss_compare.sql +0 -0
  16. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  17. {execsql2-2.22.4.data → execsql2-2.22.6.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  18. {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/WHEEL +0 -0
  19. {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/entry_points.txt +0 -0
  20. {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/licenses/LICENSE.txt +0 -0
  21. {execsql2-2.22.4.dist-info → execsql2-2.22.6.dist-info}/licenses/NOTICE +0 -0
execsql/format.py CHANGED
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
  import contextlib
15
15
  import io
16
16
  import re
17
+ from collections.abc import Iterator
17
18
  from pathlib import Path
18
19
 
19
20
  __all__ = ["collect_paths", "format_file", "main", "parse_keyword"]
@@ -169,6 +170,228 @@ def parse_keyword(payload: str) -> str:
169
170
  _EXECSQL_VAR_RE = re.compile(r"""!(['"]?)!([^!\s][^!]*)!\1!|!\{[^}]+\}!""")
170
171
 
171
172
 
173
+ # Characters that may appear in an identifier, used to tell an ``E'...'``
174
+ # escape-string prefix from a trailing ``e`` on an identifier (``abce'x'`` is
175
+ # the identifier ``abce`` followed by a separate literal).
176
+ _IDENT_CHAR_RE = re.compile(r"[A-Za-z0-9_$]")
177
+
178
+
179
+ def _iter_sql_literals(sql: str) -> Iterator[tuple[int, int, str]]:
180
+ """Yield ``(start, end, kind)`` spans for every string literal in *sql*.
181
+
182
+ This is a lexical walk rather than a regex sweep, because an apostrophe
183
+ only starts a literal in some contexts. Line comments, block comments
184
+ (which nest in PostgreSQL), and quoted identifiers are skipped entirely,
185
+ so quotes inside them never open a literal.
186
+
187
+ *kind* is one of:
188
+
189
+ ``"estring"``
190
+ A PostgreSQL escape string, ``E'...'``. Backslash escapes are
191
+ significant inside it, which is what makes it unsafe to hand to
192
+ sqlglot — see :func:`_protect_estrings`.
193
+ ``"plain"``
194
+ An ordinary ``'...'`` literal, where only a doubled ``''`` escapes.
195
+ ``"dollar"``
196
+ A dollar-quoted body, ``$$...$$`` or ``$tag$...$tag$``, in which no
197
+ character is special.
198
+ """
199
+ i, n = 0, len(sql)
200
+ while i < n:
201
+ c = sql[i]
202
+
203
+ if c == "-" and sql.startswith("--", i):
204
+ nl = sql.find("\n", i)
205
+ i = n if nl < 0 else nl + 1
206
+ continue
207
+
208
+ if c == "/" and sql.startswith("/*", i):
209
+ depth, i = 1, i + 2
210
+ while i < n and depth:
211
+ if sql.startswith("/*", i):
212
+ depth, i = depth + 1, i + 2
213
+ elif sql.startswith("*/", i):
214
+ depth, i = depth - 1, i + 2
215
+ else:
216
+ i += 1
217
+ continue
218
+
219
+ if c == '"':
220
+ i += 1
221
+ while i < n:
222
+ if sql[i] == '"':
223
+ if sql.startswith('""', i):
224
+ i += 2
225
+ continue
226
+ i += 1
227
+ break
228
+ i += 1
229
+ continue
230
+
231
+ if c == "$":
232
+ m = _DOLLAR_QUOTE_RE.match(sql, i)
233
+ if m:
234
+ tag = m.group(0)
235
+ close = sql.find(tag, m.end())
236
+ stop = n if close < 0 else close + len(tag)
237
+ yield i, stop, "dollar"
238
+ i = stop
239
+ continue
240
+
241
+ # E'...' — only when the `e` is not the tail of an identifier.
242
+ if c in "eE" and sql.startswith("'", i + 1) and not (i and _IDENT_CHAR_RE.match(sql[i - 1])):
243
+ j = i + 2
244
+ while j < n:
245
+ if sql[j] == "\\" and j + 1 < n:
246
+ j += 2
247
+ continue
248
+ if sql[j] == "'":
249
+ if sql.startswith("''", j):
250
+ j += 2
251
+ continue
252
+ j += 1
253
+ break
254
+ j += 1
255
+ yield i, j, "estring"
256
+ i = j
257
+ continue
258
+
259
+ if c == "'":
260
+ j = i + 1
261
+ while j < n:
262
+ if sql[j] == "'":
263
+ if sql.startswith("''", j):
264
+ j += 2
265
+ continue
266
+ j += 1
267
+ break
268
+ j += 1
269
+ yield i, j, "plain"
270
+ i = j
271
+ continue
272
+
273
+ i += 1
274
+
275
+
276
+ def _sql_literal_texts(sql: str) -> list[str]:
277
+ """Return the source text of every string literal in *sql*, in order."""
278
+ return [sql[start:end] for start, end, _ in _iter_sql_literals(sql)]
279
+
280
+
281
+ def _literal_line_starts(lines: list[str]) -> list[int]:
282
+ """Return the character offset at which each of *lines* starts once joined."""
283
+ starts: list[int] = []
284
+ offset = 0
285
+ for line in lines:
286
+ starts.append(offset)
287
+ offset += len(line) + 1 # +1 for the newline joined between lines
288
+ return starts
289
+
290
+
291
+ def _literal_interior_lines(lines: list[str]) -> set[int]:
292
+ """Return indices of *lines* that begin inside a multi-line string literal.
293
+
294
+ The line that *opens* a literal is ordinary SQL up to the delimiter, so it
295
+ is indented with the statement as usual. Every later line of that literal
296
+ — through and including the one holding the closing delimiter — begins
297
+ inside the string, which makes its leading whitespace part of the stored
298
+ value rather than layout. Re-indenting those lines changes what the
299
+ database receives, so callers must emit them verbatim.
300
+
301
+ This covers every literal kind the scanner reports, not just dollar-quoted
302
+ bodies: an ordinary ``'…'`` string spanning lines carries its newlines and
303
+ indentation exactly the same way.
304
+ """
305
+ if len(lines) < 2:
306
+ return set()
307
+ text = "\n".join(lines)
308
+ starts = _literal_line_starts(lines)
309
+
310
+ interior: set[int] = set()
311
+ for start, end, _kind in _iter_sql_literals(text):
312
+ interior.update(i for i, line_start in enumerate(starts) if start < line_start < end)
313
+ return interior
314
+
315
+
316
+ def _literal_spanning_lines(lines: list[str]) -> set[int]:
317
+ """Return indices of every line that any multi-line literal touches.
318
+
319
+ Unlike :func:`_literal_interior_lines` this also includes the line that
320
+ *opens* the literal. Passes that move text between lines — rather than
321
+ only adjusting leading whitespace — must leave the whole span alone, since
322
+ the opening line holds literal content after its delimiter.
323
+ """
324
+ if len(lines) < 2:
325
+ return set()
326
+ text = "\n".join(lines)
327
+ starts = _literal_line_starts(lines)
328
+
329
+ spanning: set[int] = set()
330
+ for start, end, _kind in _iter_sql_literals(text):
331
+ touched = [i for i, line_start in enumerate(starts) if start < line_start < end]
332
+ if not touched:
333
+ continue # single-line literal — nothing spans
334
+ spanning.update(touched)
335
+ spanning.add(min(touched) - 1) # the line that opens the literal
336
+ return spanning
337
+
338
+
339
+ # Placeholder standing in for a masked E'...' literal. It is a plain literal
340
+ # so that sqlglot sees a string where a string was, and lays the statement out
341
+ # the same way it would have for the real one.
342
+ _ESTR_PLACEHOLDER = "execsqlestr"
343
+ _ESTR_RESTORE_RE = re.compile(rf"'{_ESTR_PLACEHOLDER}(\d+)'")
344
+
345
+
346
+ def _protect_estrings(sql: str) -> tuple[str, list[str]]:
347
+ """Replace ``E'...'`` literals with placeholders, return (protected, originals).
348
+
349
+ sqlglot's Postgres tokenizer consumes backslash escapes inside an escape
350
+ string but its generator never re-emits them, so ``E'\\\\s+'`` round-trips
351
+ to ``e'\\s+'`` — a different string (tobymao/sqlglot#8191). Masking these
352
+ literals keeps them out of the round trip entirely and restores them
353
+ byte-for-byte, independently of the installed sqlglot version.
354
+ """
355
+ originals: list[str] = []
356
+ out: list[str] = []
357
+ last = 0
358
+ for start, end, kind in _iter_sql_literals(sql):
359
+ if kind != "estring":
360
+ continue
361
+ out.append(sql[last:start])
362
+ out.append(f"'{_ESTR_PLACEHOLDER}{len(originals)}'")
363
+ originals.append(sql[start:end])
364
+ last = end
365
+ if not originals:
366
+ return sql, []
367
+ out.append(sql[last:])
368
+ return "".join(out), originals
369
+
370
+
371
+ def _restore_estrings(sql: str, originals: list[str]) -> str | None:
372
+ """Put masked ``E'...'`` literals back; return None if any went missing.
373
+
374
+ A missing placeholder means sqlglot dropped or mangled the literal, so the
375
+ caller must fall back to the unformatted text rather than emit SQL with a
376
+ placeholder left in it.
377
+ """
378
+ if not originals:
379
+ return sql
380
+ seen: set[int] = set()
381
+
382
+ def replace(m: re.Match[str]) -> str:
383
+ idx = int(m.group(1))
384
+ if idx >= len(originals):
385
+ return m.group(0)
386
+ seen.add(idx)
387
+ return originals[idx]
388
+
389
+ restored = _ESTR_RESTORE_RE.sub(replace, sql)
390
+ if len(seen) != len(originals) or _ESTR_PLACEHOLDER in restored:
391
+ return None
392
+ return restored
393
+
394
+
172
395
  def _protect_variables(sql: str) -> tuple[str, list[tuple[str, str]]]:
173
396
  """Replace execsql substitutions with valid SQL identifiers, return (protected, replacements)."""
174
397
  replacements: list[tuple[str, str]] = []
@@ -224,6 +447,9 @@ def _sqlglot_format(
224
447
 
225
448
  text = "\n".join(sql_lines)
226
449
  protected, replacements = _protect_variables(text)
450
+ # Keep E'...' literals out of the round trip; sqlglot loses their
451
+ # backslash escapes (see _protect_estrings).
452
+ protected, estrings = _protect_estrings(protected)
227
453
 
228
454
  # Count semicolons in input as a rough statement count.
229
455
  input_semis = protected.count(";")
@@ -275,8 +501,29 @@ def _sqlglot_format(
275
501
  output_alnum_len = len(_alnum.sub("", joined))
276
502
  if input_alnum_len and output_alnum_len < input_alnum_len * 0.7:
277
503
  return sql_lines
504
+
505
+ # Literal round-trip check. The alphanumeric check above is a
506
+ # gross-token-loss detector: it deletes every non-alphanumeric
507
+ # character before counting, so a transformation that changes only
508
+ # punctuation — a dropped backslash, a swapped quote — is invisible to
509
+ # it at any threshold. Requiring every literal to survive verbatim
510
+ # closes that blind spot for the whole class, not just for the E'...'
511
+ # case masked above.
512
+ #
513
+ # Containment, not equality: sqlglot rewrites dialect-specific
514
+ # constructs into Postgres equivalents that can legitimately repeat a
515
+ # literal (``SUBSTRING_INDEX(d, ',', 1)`` expands into a form using
516
+ # ``','`` more than once), so the output may hold more literals than
517
+ # the input. What must never happen is an input literal coming back
518
+ # altered or not at all — that is content loss, and it falls back.
519
+ if set(_sql_literal_texts(protected)) - set(_sql_literal_texts(joined)):
520
+ return sql_lines
521
+
278
522
  joined = re.sub(r"\bINTO TEMPORARY\b(?!\s+TABLE)", "INTO TEMPORARY TABLE", joined)
279
- return _restore_variables(joined, replacements).split("\n")
523
+ restored = _restore_estrings(joined, estrings)
524
+ if restored is None:
525
+ return sql_lines
526
+ return _restore_variables(restored, replacements).split("\n")
280
527
  except Exception:
281
528
  return sql_lines
282
529
 
@@ -314,6 +561,14 @@ def _has_mid_statement_comments(lines: list[str]) -> bool:
314
561
 
315
562
  _CMT_MARKER = "EXECSQL_CMTMARKER_"
316
563
  _CMT_MARKER_RE = re.compile(rf"/\*\s*({re.escape(_CMT_MARKER)}\d+)\s*\*/")
564
+ # A whole run of adjacent markers plus the whitespace injected around them.
565
+ # Markers are written as ``/* marker */ line`` and several consecutive source
566
+ # comments attach several markers to one line, so removing them one at a time
567
+ # leaves a separator space behind for each. When sqlglot moves the run into
568
+ # the middle of an expression that residue shows up as ``AND RTRIM(`` and is
569
+ # only normalised on the following pass, which costs an extra pass to
570
+ # converge. Matching the run as a unit removes it cleanly. See issue #35.
571
+ _CMT_MARKER_RUN_RE = re.compile(rf"\s*(?:/\*\s*{re.escape(_CMT_MARKER)}\d+\s*\*/\s*)+")
317
572
 
318
573
 
319
574
  def _format_preserving_comments(
@@ -381,12 +636,16 @@ def _format_preserving_comments(
381
636
  markers_here = _CMT_MARKER_RE.findall(fline)
382
637
  if markers_here:
383
638
  # Strip markers to get the underlying SQL line and its indent
384
- cleaned = _CMT_MARKER_RE.sub("", fline).strip()
385
- # Determine indent: use the SQL line's indent from sqlglot
386
- line_indent = ""
387
- if cleaned:
388
- raw_cleaned = _CMT_MARKER_RE.sub("", fline)
389
- line_indent = raw_cleaned[: len(raw_cleaned) - len(raw_cleaned.lstrip())]
639
+ cleaned = _CMT_MARKER_RUN_RE.sub(" ", fline).strip()
640
+ # Indent comes from the line's own leading whitespace, never from
641
+ # what is left after removing the marker. Markers are injected as
642
+ # ``/* marker */ line`` — with a separating space — so measuring
643
+ # after removal counts that space as indentation. Whenever
644
+ # sqlglot leaves the line untouched (an unparsable statement falls
645
+ # back verbatim) the extra space is re-measured and re-added on the
646
+ # next run, growing the indent by one on every pass and never
647
+ # converging. See issue #35.
648
+ line_indent = fline[: len(fline) - len(fline.lstrip())]
390
649
  for m in markers_here:
391
650
  if m in comment_store:
392
651
  orig = comment_store[m]
@@ -482,12 +741,24 @@ def format_sql_block(
482
741
  if not non_empty:
483
742
  return [""] * len(lines)
484
743
 
485
- base = min(len(line) - len(line.lstrip()) for line in non_empty)
744
+ # Lines that begin inside a multi-line literal carry content, not layout —
745
+ # dedenting or indenting them changes the value the database stores.
746
+ # Exclude them from the indent arithmetic and emit them verbatim.
747
+ interior = _literal_interior_lines(lines)
748
+ if interior:
749
+ # format_file already withholds dollar-quoted blocks from sqlglot; keep
750
+ # that true here so the line indices below stay aligned with the output.
751
+ use_sql = False
752
+
753
+ layout_lines = [line for i, line in enumerate(lines) if line.strip() and i not in interior]
754
+ base = min((len(line) - len(line.lstrip()) for line in layout_lines), default=0)
486
755
  target_prefix = " " * (depth * indent)
487
- rebased = [line[base:] if line.strip() else "" for line in lines]
756
+ rebased = [line if i in interior else (line[base:] if line.strip() else "") for i, line in enumerate(lines)]
488
757
 
489
758
  if not use_sql:
490
- return [target_prefix + line if line.strip() else "" for line in rebased]
759
+ return [
760
+ line if i in interior else (target_prefix + line if line.strip() else "") for i, line in enumerate(rebased)
761
+ ]
491
762
 
492
763
  # When comments appear mid-statement, use the marker-based approach
493
764
  # which preserves both comments AND sqlglot formatting. When all
@@ -558,6 +829,9 @@ def _normalize_to_trailing_comma(text: str) -> str:
558
829
  every invocation. Comments between the two SQL lines stay in place.
559
830
  """
560
831
  lines = text.split("\n")
832
+ # Symmetric with _apply_leading_comma: a leading comma inside a multi-line
833
+ # literal is data, not layout, so the whole span is left alone.
834
+ protected = _literal_spanning_lines(lines)
561
835
 
562
836
  def find_prev_sql_line(idx: int) -> int:
563
837
  k = idx - 1
@@ -567,7 +841,7 @@ def _normalize_to_trailing_comma(text: str) -> str:
567
841
 
568
842
  for i, line in enumerate(lines):
569
843
  stripped = line.lstrip()
570
- if not stripped.startswith(",") or _is_comment_only(line):
844
+ if not stripped.startswith(",") or _is_comment_only(line) or i in protected:
571
845
  continue
572
846
  # Don't try to rewrite leading-comma inside a comment-only line.
573
847
  # Move the comma onto the previous SQL line as a trailing `,`.
@@ -603,15 +877,19 @@ def _apply_leading_comma(text: str) -> str:
603
877
  TestIdempotency).
604
878
  """
605
879
  lines = text.split("\n")
880
+ # A comma inside a multi-line string literal is data. Moving it to the next
881
+ # line would rewrite the stored value, so leave every line such a literal
882
+ # touches — including the one that opens it — exactly as it is.
883
+ protected = _literal_spanning_lines(lines)
606
884
  i = 0
607
885
  n = len(lines)
608
886
  while i < n:
609
887
  rstripped = lines[i].rstrip()
610
- if rstripped.endswith(",") and not _is_comment_only(lines[i]):
888
+ if rstripped.endswith(",") and not _is_comment_only(lines[i]) and i not in protected:
611
889
  j = i + 1
612
890
  while j < n and (not lines[j].strip() or _is_comment_only(lines[j])):
613
891
  j += 1
614
- if j < n:
892
+ if j < n and j not in protected:
615
893
  stripped = lines[i].rstrip()
616
894
  comma_idx = stripped.rfind(",")
617
895
  lines[i] = stripped[:comma_idx] + stripped[comma_idx + 1 :]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.22.4
3
+ Version: 2.22.6
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
@@ -384,7 +384,7 @@ execsql-format --no-sql --in-place scripts/
384
384
  ```yaml
385
385
  repos:
386
386
  - repo: https://github.com/geocoug/execsql
387
- rev: v2.22.4
387
+ rev: v2.22.6
388
388
  hooks:
389
389
  - id: execsql-format
390
390
  ```
@@ -3,7 +3,7 @@ execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
3
3
  execsql/api.py,sha256=EOtBwMnm-8fmB_KmvxyjglR6C7CjdYhCPBisJ9CFfJg,22227
4
4
  execsql/config.py,sha256=txKziUJLV7UPonj2sGFgkmlinDdSV50aHmR_cuCbEIo,34369
5
5
  execsql/exceptions.py,sha256=GlphAiR8iZu4UR4U50WgSqEUMd9_8AeQJ_hhPcJOeQU,9381
6
- execsql/format.py,sha256=3w-m0EsHapBdSKx6oTFWYlXspFLdUR_RF79gMvHhrqQ,34440
6
+ execsql/format.py,sha256=YJH_JA1_IqBfeWhgrusA3KKs_xrmbm3ktuVf_xJryws,46017
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
@@ -101,23 +101,23 @@ execsql/utils/numeric.py,sha256=_7bi5R6pHjMCkOPne_Sh5nUj-B8e9g6WFmjptFHaunA,1573
101
101
  execsql/utils/regex.py,sha256=B3mSBhRRx_DpUnX_t9IoYd05KvWAebiLMCZqQAmEwcw,3885
102
102
  execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
103
103
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
104
- execsql2-2.22.4.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
105
- execsql2-2.22.4.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
106
- execsql2-2.22.4.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
107
- execsql2-2.22.4.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
108
- execsql2-2.22.4.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
109
- execsql2-2.22.4.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
110
- execsql2-2.22.4.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
111
- execsql2-2.22.4.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
112
- execsql2-2.22.4.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
113
- execsql2-2.22.4.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
114
- execsql2-2.22.4.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
115
- execsql2-2.22.4.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
116
- execsql2-2.22.4.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
117
- execsql2-2.22.4.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
118
- execsql2-2.22.4.dist-info/METADATA,sha256=gD2Dm8BheKQrFz6xAs7KyfpqGzs93Rpv7vxWyPWXeQE,20382
119
- execsql2-2.22.4.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
120
- execsql2-2.22.4.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
121
- execsql2-2.22.4.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
122
- execsql2-2.22.4.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
123
- execsql2-2.22.4.dist-info/RECORD,,
104
+ execsql2-2.22.6.data/data/execsql2_extras/README.md,sha256=vX4NTL095dUoA_hesyRMGYBorEZ_Y_tJ9qrd-MVV09I,5032
105
+ execsql2-2.22.6.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
106
+ execsql2-2.22.6.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
107
+ execsql2-2.22.6.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
108
+ execsql2-2.22.6.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
109
+ execsql2-2.22.6.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
110
+ execsql2-2.22.6.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
111
+ execsql2-2.22.6.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
112
+ execsql2-2.22.6.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
113
+ execsql2-2.22.6.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
114
+ execsql2-2.22.6.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
115
+ execsql2-2.22.6.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
116
+ execsql2-2.22.6.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
117
+ execsql2-2.22.6.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
118
+ execsql2-2.22.6.dist-info/METADATA,sha256=zX8fCinRuVHLFNIAKypooRUh_qPFfn-J6oBwu30_J_U,20382
119
+ execsql2-2.22.6.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
120
+ execsql2-2.22.6.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
121
+ execsql2-2.22.6.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
122
+ execsql2-2.22.6.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
123
+ execsql2-2.22.6.dist-info/RECORD,,