execsql2 2.16.7__py3-none-any.whl → 2.16.12__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 (29) hide show
  1. execsql/debug/repl.py +59 -0
  2. execsql/format.py +2 -0
  3. execsql/metacommands/debug.py +89 -1
  4. execsql/metacommands/dispatch.py +18 -0
  5. execsql/script/ast.py +43 -6
  6. execsql/script/executor.py +28 -13
  7. execsql/script/parser.py +175 -26
  8. execsql/utils/fileio.py +9 -1
  9. {execsql2-2.16.7.dist-info → execsql2-2.16.12.dist-info}/METADATA +1 -1
  10. {execsql2-2.16.7.dist-info → execsql2-2.16.12.dist-info}/RECORD +29 -29
  11. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/README.md +0 -0
  12. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  13. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  14. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/execsql.conf +0 -0
  15. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/make_config_db.sql +0 -0
  16. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/md_compare.sql +0 -0
  17. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/md_glossary.sql +0 -0
  18. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/md_upsert.sql +0 -0
  19. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/pg_compare.sql +0 -0
  20. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  21. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  22. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/script_template.sql +0 -0
  23. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/ss_compare.sql +0 -0
  24. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  25. {execsql2-2.16.7.data → execsql2-2.16.12.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  26. {execsql2-2.16.7.dist-info → execsql2-2.16.12.dist-info}/WHEEL +0 -0
  27. {execsql2-2.16.7.dist-info → execsql2-2.16.12.dist-info}/entry_points.txt +0 -0
  28. {execsql2-2.16.7.dist-info → execsql2-2.16.12.dist-info}/licenses/LICENSE.txt +0 -0
  29. {execsql2-2.16.7.dist-info → execsql2-2.16.12.dist-info}/licenses/NOTICE +0 -0
execsql/debug/repl.py CHANGED
@@ -111,6 +111,8 @@ _HELP_COMMANDS = [
111
111
  (".where", ".w", "Show the current script location and upcoming statement"),
112
112
  (".stack", "", "Show the command-list stack (script name, line, depth)"),
113
113
  (".set VAR VAL", ".s", "Set or update a substitution variable"),
114
+ (".scripts", "", "List all registered SCRIPT definitions"),
115
+ (".scripts NAME", "", "Show detail for a specific SCRIPT"),
114
116
  (".help", ".h", "Show this help text"),
115
117
  ]
116
118
 
@@ -290,6 +292,12 @@ def _handle_dot_command(line: str) -> None:
290
292
  varname = parts[0]
291
293
  value = parts[1] if len(parts) > 1 else ""
292
294
  _set_var(varname, value)
295
+ elif cmd.startswith("scripts"):
296
+ rest = cmd[7:].strip()
297
+ if rest:
298
+ _print_script_detail(rest)
299
+ else:
300
+ _print_scripts()
293
301
  else:
294
302
  _write(f" {_c(_RED, 'Unknown command:')} {line!r}. Type '.help' for available commands.\n")
295
303
 
@@ -507,3 +515,54 @@ def _set_var(varname: str, value: str) -> None:
507
515
  else:
508
516
  subvars.add_substitution(varname, value)
509
517
  _write(f" {_c(_CYAN, varname)} {_c(_DIM, '=')} {value}\n")
518
+
519
+
520
+ def _print_scripts() -> None:
521
+ """Print all registered SCRIPT definitions."""
522
+ from execsql.metacommands.debug import _format_script_signature, _format_script_source
523
+
524
+ scripts = _state.ast_scripts
525
+ if not scripts:
526
+ _write(" (no scripts registered)\n\n")
527
+ return
528
+ _write_rule(f" {_c(_BOLD + _YELLOW, 'Scripts')} {_c(_DIM, f'({len(scripts)})')} ")
529
+ sigs = {name: _format_script_signature(name, block.param_defs) for name, block in scripts.items()}
530
+ max_sig = max(len(s) for s in sigs.values())
531
+ for name, block in scripts.items():
532
+ sig = sigs[name]
533
+ src = _format_script_source(block.span)
534
+ _write(f" {_c(_CYAN, sig):<{max_sig + len(_CYAN) + len(_RESET)}} {_c(_DIM, src)}\n")
535
+ _write("\n")
536
+
537
+
538
+ def _print_script_detail(name: str) -> None:
539
+ """Print detail for a single SCRIPT definition."""
540
+ from execsql.metacommands.debug import _format_script_signature, _format_script_source
541
+
542
+ script_name = name.lower()
543
+ scripts = _state.ast_scripts
544
+ if script_name not in scripts:
545
+ _write(f" {_c(_RED, 'No script named')} {_c(_CYAN, repr(script_name))} {_c(_RED, 'is registered.')}\n")
546
+ return
547
+ block = scripts[script_name]
548
+ sig = _format_script_signature(block.name, block.param_defs)
549
+ src = _format_script_source(block.span)
550
+ _write_rule(f" {_c(_BOLD + _YELLOW, 'Script')} {_c(_DIM, '──')} {_c(_CYAN, sig)} ")
551
+ _write(f" {_c(_BOLD, 'Source:')} {src}\n")
552
+ if block.param_defs:
553
+ _write(f" {_c(_BOLD, 'Parameters:')}\n")
554
+ max_name = max(len(p.name) for p in block.param_defs)
555
+ for p in block.param_defs:
556
+ if p.default is not None:
557
+ _write(
558
+ f" {_c(_CYAN, p.name):<{max_name + len(_CYAN) + len(_RESET)}} {_c(_DIM, f'(optional, default: {p.default})')}\n",
559
+ )
560
+ else:
561
+ _write(f" {_c(_CYAN, p.name):<{max_name + len(_CYAN) + len(_RESET)}} {_c(_DIM, '(required)')}\n")
562
+ else:
563
+ _write(f" {_c(_BOLD, 'Parameters:')} (none)\n")
564
+ if block.doc:
565
+ _write("\n")
566
+ for doc_line in block.doc.split("\n"):
567
+ _write(f" {_c(_DIM, doc_line)}\n")
568
+ _write("\n")
execsql/format.py CHANGED
@@ -73,6 +73,8 @@ MULTIWORD_KEYWORDS = [
73
73
  "PROMPT ASK",
74
74
  "WITH TEMPLATE",
75
75
  "IN ZIPFILE",
76
+ "SHOW SCRIPTS",
77
+ "SHOW SCRIPT",
76
78
  ]
77
79
 
78
80
  # Depth-tracking sets
@@ -1,5 +1,4 @@
1
1
  from __future__ import annotations
2
- from execsql.utils.errors import fatal_error
3
2
 
4
3
  """
5
4
  Debug metacommand handlers for execsql.
@@ -7,11 +6,16 @@ Debug metacommand handlers for execsql.
7
6
  Provides ``x_debug_write_metacommands``, which implements the
8
7
  ``WRITE METACOMMANDS`` debug metacommand that prints the full registered
9
8
  metacommand list to the log/console for troubleshooting.
9
+
10
+ Also provides ``x_show_scripts`` and ``x_show_script`` for runtime
11
+ introspection of registered SCRIPT blocks.
10
12
  """
11
13
 
14
+ from pathlib import Path
12
15
  from typing import Any
13
16
 
14
17
  import execsql.state as _state
18
+ from execsql.utils.errors import fatal_error
15
19
  from execsql.utils.fileio import EncodedFile, filewriter_open_as_new, filewriter_write
16
20
 
17
21
 
@@ -173,3 +177,87 @@ def x_debug_write_config(**kwargs: Any) -> None:
173
177
 
174
178
  for line in lines:
175
179
  write(f"{line}\n")
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Helpers for SCRIPT introspection (shared by metacommands and REPL)
184
+ # ---------------------------------------------------------------------------
185
+
186
+
187
+ def _format_script_signature(name: str, param_defs: Any) -> str:
188
+ """Return ``name(param1, param2, opt=default)`` or ``name()``.
189
+
190
+ *param_defs* may be a list of :class:`ParamDef` objects (preferred) or
191
+ a plain list of strings (backward compat).
192
+ """
193
+ if not param_defs:
194
+ return f"{name}()"
195
+ parts: list[str] = []
196
+ for p in param_defs:
197
+ if hasattr(p, "default") and p.default is not None:
198
+ parts.append(f"{p.name}={p.default}")
199
+ elif hasattr(p, "name"):
200
+ parts.append(p.name)
201
+ else:
202
+ parts.append(str(p))
203
+ return f"{name}({', '.join(parts)})"
204
+
205
+
206
+ def _format_script_source(span: Any) -> str:
207
+ """Return ``file:start-end`` from a SourceSpan."""
208
+ filename = Path(span.file).name if span and span.file else "<unknown>"
209
+ if span and span.start_line is not None:
210
+ if span.end_line is not None and span.end_line != span.start_line:
211
+ return f"{filename}:{span.start_line}-{span.end_line}"
212
+ return f"{filename}:{span.start_line}"
213
+ return filename
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # SHOW SCRIPTS / SHOW SCRIPT metacommand handlers
218
+ # ---------------------------------------------------------------------------
219
+
220
+
221
+ def x_show_scripts(**kwargs: Any) -> None:
222
+ """List all registered SCRIPT definitions with parameters and source location."""
223
+ scripts = _state.ast_scripts
224
+ if not scripts:
225
+ _state.output.write("No scripts registered.\n")
226
+ return
227
+ _state.output.write(f"Registered scripts ({len(scripts)}):\n\n")
228
+ # Compute column width for alignment
229
+ sigs = {name: _format_script_signature(name, block.param_defs) for name, block in scripts.items()}
230
+ max_sig = max(len(s) for s in sigs.values())
231
+ for name, block in scripts.items():
232
+ sig = sigs[name]
233
+ src = _format_script_source(block.span)
234
+ _state.output.write(f" {sig:<{max_sig}} {src}\n")
235
+ _state.output.write("\n")
236
+
237
+
238
+ def x_show_script(**kwargs: Any) -> None:
239
+ """Show detail for a single registered SCRIPT definition."""
240
+ script_name = kwargs.get("script_id", "").lower()
241
+ scripts = _state.ast_scripts
242
+ if script_name not in scripts:
243
+ _state.output.write(f"No script named '{script_name}' is registered.\n")
244
+ return
245
+ block = scripts[script_name]
246
+ sig = _format_script_signature(block.name, block.param_defs)
247
+ src = _format_script_source(block.span)
248
+ _state.output.write(f"Script: {sig}\n")
249
+ _state.output.write(f"Source: {src}\n")
250
+ if block.param_defs:
251
+ _state.output.write("Parameters:\n")
252
+ max_name = max(len(p.name) for p in block.param_defs)
253
+ for p in block.param_defs:
254
+ if p.default is not None:
255
+ _state.output.write(f" {p.name:<{max_name}} (optional, default: {p.default})\n")
256
+ else:
257
+ _state.output.write(f" {p.name:<{max_name}} (required)\n")
258
+ else:
259
+ _state.output.write("Parameters: (none)\n")
260
+ if block.doc:
261
+ _state.output.write("\n")
262
+ for doc_line in block.doc.split("\n"):
263
+ _state.output.write(f" {doc_line}\n")
@@ -99,6 +99,8 @@ from execsql.metacommands.debug import (
99
99
  x_debug_write_metacommands,
100
100
  x_debug_write_odbc_drivers,
101
101
  x_debug_write_subvars,
102
+ x_show_script,
103
+ x_show_scripts,
102
104
  )
103
105
  from execsql.debug.repl import x_breakpoint
104
106
  from execsql.metacommands.io import (
@@ -1747,6 +1749,22 @@ def build_dispatch_table() -> MetaCommandList:
1747
1749
  run_when_false=False,
1748
1750
  )
1749
1751
 
1752
+ # ------------------------------------------------------------------
1753
+ # SHOW SCRIPTS / SHOW SCRIPT
1754
+ # ------------------------------------------------------------------
1755
+ mcl.add(
1756
+ r"^\s*SHOW\s+SCRIPTS\s*$",
1757
+ x_show_scripts,
1758
+ description="SHOW SCRIPTS",
1759
+ category="action",
1760
+ )
1761
+ mcl.add(
1762
+ r"^\s*SHOW\s+SCRIPT\s+(?P<script_id>\w+)\s*$",
1763
+ x_show_script,
1764
+ description="SHOW SCRIPT",
1765
+ category="action",
1766
+ )
1767
+
1750
1768
  # ------------------------------------------------------------------
1751
1769
  # IF / ORIF / ANDIF / ELSEIF / ELSE / ENDIF
1752
1770
  # ------------------------------------------------------------------
execsql/script/ast.py CHANGED
@@ -52,6 +52,7 @@ __all__ = [
52
52
  "IfBlock",
53
53
  "LoopBlock",
54
54
  "BatchBlock",
55
+ "ParamDef",
55
56
  "ScriptBlock",
56
57
  "SqlBlock",
57
58
  "IncludeDirective",
@@ -290,6 +291,24 @@ class BatchBlock(Node):
290
291
  return f"BatchBlock({self.span}, body={len(self.body)})"
291
292
 
292
293
 
294
+ @dataclass(frozen=True, slots=True)
295
+ class ParamDef:
296
+ """A single SCRIPT parameter definition with an optional default value.
297
+
298
+ Attributes:
299
+ name: The parameter name (as declared, without ``#`` prefix).
300
+ default: The default value string, or ``None`` for required parameters.
301
+ """
302
+
303
+ name: str
304
+ default: str | None = None
305
+
306
+ @property
307
+ def required(self) -> bool:
308
+ """Return ``True`` if this parameter has no default value."""
309
+ return self.default is None
310
+
311
+
293
312
  @dataclass
294
313
  class ScriptBlock(Node):
295
314
  """A BEGIN SCRIPT name ... END SCRIPT structure.
@@ -299,20 +318,31 @@ class ScriptBlock(Node):
299
318
 
300
319
  Attributes:
301
320
  name: The script block name (lowercased).
302
- param_names: Optional list of formal parameter names.
321
+ param_defs: Optional list of :class:`ParamDef` parameter definitions.
322
+ doc: Optional docstring extracted from comments immediately following
323
+ the BEGIN SCRIPT line.
303
324
  body: Nodes within the script block.
304
325
  """
305
326
 
306
327
  name: str
307
- param_names: list[str] | None = None
328
+ param_defs: list[ParamDef] | None = None
329
+ doc: str | None = None
308
330
  body: list[Node] = field(default_factory=list)
309
331
 
332
+ @property
333
+ def param_names(self) -> list[str] | None:
334
+ """Return parameter names only (backward compatibility)."""
335
+ if self.param_defs is None:
336
+ return None
337
+ return [p.name for p in self.param_defs]
338
+
310
339
  def children(self) -> Iterator[Node]:
311
340
  yield from self.body
312
341
 
313
342
  def __repr__(self) -> str:
314
- params = f", params={self.param_names}" if self.param_names else ""
315
- return f"ScriptBlock({self.span}, name={self.name!r}{params}, body={len(self.body)})"
343
+ params = f", params={self.param_names}" if self.param_defs else ""
344
+ doc_tag = ", doc=True" if self.doc else ""
345
+ return f"ScriptBlock({self.span}, name={self.name!r}{params}{doc_tag}, body={len(self.body)})"
316
346
 
317
347
 
318
348
  @dataclass
@@ -547,8 +577,15 @@ def _node_label(node: Node) -> str:
547
577
  if isinstance(node, BatchBlock):
548
578
  return f"{_tag('BATCH')} BEGIN BATCH"
549
579
  if isinstance(node, ScriptBlock):
550
- params = f" ({', '.join(node.param_names)})" if node.param_names else ""
551
- return f"{_tag('SCRIPT')} {node.name}{params}"
580
+ if node.param_defs:
581
+ parts = []
582
+ for p in node.param_defs:
583
+ parts.append(f"{p.name}={p.default}" if p.default is not None else p.name)
584
+ params = f" ({', '.join(parts)})"
585
+ else:
586
+ params = ""
587
+ doc_tag = " [doc]" if node.doc else ""
588
+ return f"{_tag('SCRIPT')} {node.name}{params}{doc_tag}"
552
589
  if isinstance(node, SqlBlock):
553
590
  return f"{_tag('SQL_BLK')} BEGIN SQL"
554
591
  if isinstance(node, IncludeDirective):
@@ -648,7 +648,10 @@ def _execute_include(
648
648
  WHILE/UNTIL loops are handled natively too.
649
649
  """
650
650
  if node.is_execute_script:
651
- target = node.target.lower()
651
+ # Substitute variables in the target the script name may be passed
652
+ # as a parameter (e.g., EXECUTE SCRIPT !!#script_name!!).
653
+ effective_locals = _stack_localvars(ctx) or localvars
654
+ target = substitute_vars(node.target, effective_locals, ctx=ctx).strip().lower()
652
655
 
653
656
  # Native path: target is in our AST registry
654
657
  if target in ctx.ast_scripts:
@@ -713,23 +716,35 @@ def _execute_script_native(
713
716
  for param, arg in all_prepared_args:
714
717
  paramvals.add_substitution(param, arg)
715
718
 
716
- # Validate parameter names match
717
- if script_block.param_names is not None:
719
+ # Validate parameter names match — with default parameter support
720
+ if script_block.param_defs is not None:
718
721
  passed_names = [p[0].lstrip("#") for p in all_prepared_args]
719
- if not all(p in passed_names for p in script_block.param_names):
722
+ required = [p.name for p in script_block.param_defs if p.required]
723
+ missing = [p for p in required if p not in passed_names]
724
+ if missing:
720
725
  raise ErrInfo(
721
726
  "error",
722
- other_msg=f"Formal and actual parameter name mismatch in call to {script_block.name}.",
727
+ other_msg=(f"Missing required parameter(s) ({', '.join(missing)}) in call to {script_block.name}."),
723
728
  )
729
+ # Inject defaults for optional params not provided
730
+ for pdef in script_block.param_defs:
731
+ if pdef.default is not None and pdef.name not in passed_names:
732
+ paramvals.add_substitution(f"#{pdef.name}", pdef.default)
724
733
  else:
725
- if script_block.param_names is not None:
726
- raise ErrInfo(
727
- "error",
728
- other_msg=(
729
- f"Missing expected parameters ({', '.join(script_block.param_names)}) "
730
- f"in call to {script_block.name}."
731
- ),
732
- )
734
+ if script_block.param_defs is not None:
735
+ required = [p.name for p in script_block.param_defs if p.required]
736
+ if required:
737
+ raise ErrInfo(
738
+ "error",
739
+ other_msg=(
740
+ f"Missing required parameter(s) ({', '.join(required)}) in call to {script_block.name}."
741
+ ),
742
+ )
743
+ # No args provided but all params have defaults — inject them all
744
+ paramvals = ScriptArgSubVarSet()
745
+ for pdef in script_block.param_defs:
746
+ if pdef.default is not None:
747
+ paramvals.add_substitution(f"#{pdef.name}", pdef.default)
733
748
 
734
749
  # Push a CommandList frame onto the stack so that:
735
750
  # - get_subvarset() can find ~local and +outer-scope variables
execsql/script/parser.py CHANGED
@@ -42,6 +42,7 @@ from execsql.script.ast import (
42
42
  LoopBlock,
43
43
  MetaCommandStatement,
44
44
  Node,
45
+ ParamDef,
45
46
  Script,
46
47
  ScriptBlock,
47
48
  SourceSpan,
@@ -106,7 +107,7 @@ def _strip_quotes(s: str) -> str:
106
107
  _EXEC_SCRIPT_RX = re.compile(
107
108
  r"^\s*(?:EXEC(?:UTE)?|RUN)\s+SCRIPT"
108
109
  r"(?P<exists>\s+IF\s+EXISTS)?"
109
- r"\s+(?P<script_id>\w+)"
110
+ r"\s+(?P<script_id>(?:\w+|!(?:['\"]?)![^!]+!(?:['\"]?)!))"
110
111
  r"(?:(?:\s+WITH)?(?:\s+ARG(?:UMENT)?S?)?\s*\(\s*(?P<argexp>.+?)\s*\))?"
111
112
  r"(?:\s+(?P<looptype>WHILE|UNTIL)\s*\(\s*(?P<loopcond>.+)\s*\))?"
112
113
  r"\s*$",
@@ -114,10 +115,52 @@ _EXEC_SCRIPT_RX = re.compile(
114
115
  )
115
116
 
116
117
  _WITH_PARAMS_RX = re.compile(
117
- r"(?:\s+WITH)?(?:\s+PARAM(?:ETER)?S)?\s*\(\s*(?P<params>\w+(?:\s*,\s*\w+)*)\s*\)\s*$",
118
+ r"(?:\s+WITH)?(?:\s+PARAM(?:ETER)?S)?\s*\(\s*(?P<params>"
119
+ r"\w+(?:\s*=\s*\S+)?(?:\s*,\s*\w+(?:\s*=\s*\S+)?)*"
120
+ r")\s*\)\s*$",
118
121
  re.I,
119
122
  )
120
123
 
124
+ _PARAM_TOKEN_RX = re.compile(r"(\w+)(?:\s*=\s*(\S+))?")
125
+
126
+
127
+ def _parse_param_defs(
128
+ params_str: str,
129
+ lineno: int,
130
+ source: str,
131
+ ) -> list[ParamDef]:
132
+ """Parse ``'a, b, c=100, d=false'`` into a list of :class:`ParamDef`.
133
+
134
+ Required parameters (no default) must precede optional parameters
135
+ (with default). Raises :class:`ErrInfo` if ordering is violated.
136
+ """
137
+ tokens = [t.strip() for t in params_str.split(",")]
138
+ defs: list[ParamDef] = []
139
+ seen_optional: str | None = None # name of first optional param
140
+ for token in tokens:
141
+ m = _PARAM_TOKEN_RX.match(token.strip())
142
+ if not m:
143
+ raise ErrInfo(
144
+ type="cmd",
145
+ other_msg=(f"Invalid parameter token '{token}' on line {lineno} of {source}."),
146
+ )
147
+ name, default = m.group(1), m.group(2)
148
+ if default is not None:
149
+ if seen_optional is None:
150
+ seen_optional = name
151
+ elif seen_optional is not None:
152
+ raise ErrInfo(
153
+ type="cmd",
154
+ other_msg=(
155
+ f"Required parameter '{name}' after optional parameter "
156
+ f"'{seen_optional}' on line {lineno} of {source}. "
157
+ f"Required parameters must precede optional parameters."
158
+ ),
159
+ )
160
+ defs.append(ParamDef(name=name, default=default))
161
+ return defs
162
+
163
+
121
164
  # Line classification
122
165
  _EXEC_LINE_RX = re.compile(r"^\s*--\s*!x!\s*(?P<cmd>.+)$", re.I)
123
166
  _COMMENT_LINE_RX = re.compile(r"^\s*--")
@@ -135,7 +178,16 @@ class _BlockFrame:
135
178
  to close the block correctly.
136
179
  """
137
180
 
138
- __slots__ = ("node", "kind", "start_line", "source", "_in_else", "_in_elseif")
181
+ __slots__ = (
182
+ "node",
183
+ "kind",
184
+ "start_line",
185
+ "source",
186
+ "_in_else",
187
+ "_in_elseif",
188
+ "collecting_doc",
189
+ "doc_lines",
190
+ )
139
191
 
140
192
  def __init__(self, node: Node, kind: str, start_line: int, source: str) -> None:
141
193
  self.node = node
@@ -144,6 +196,8 @@ class _BlockFrame:
144
196
  self.source = source
145
197
  self._in_else = False
146
198
  self._in_elseif = False
199
+ self.collecting_doc: bool = kind == "script" # auto-collect doc after BEGIN SCRIPT
200
+ self.doc_lines: list[str] = []
147
201
 
148
202
 
149
203
  # ---------------------------------------------------------------------------
@@ -163,6 +217,7 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
163
217
  in_sql_block = False # inside BEGIN SQL ... END SQL
164
218
  sql_accum = "" # multi-line SQL accumulator
165
219
  sql_start_line = 0
220
+ sql_accum_at_block_comment = False # was sql_accum non-empty when /* started?
166
221
 
167
222
  def _current_body() -> list[Node]:
168
223
  """Return the body list that new nodes should be appended to."""
@@ -205,9 +260,67 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
205
260
  )
206
261
  line_comment_lines = []
207
262
 
263
+ def _stop_doc_collection() -> None:
264
+ """Stop collecting docstring lines and finalize the doc on the script node."""
265
+ if block_stack and block_stack[-1].collecting_doc:
266
+ frame = block_stack[-1]
267
+ frame.collecting_doc = False
268
+ if frame.doc_lines:
269
+ doc_text = "\n".join(frame.doc_lines).strip()
270
+ if doc_text and isinstance(frame.node, ScriptBlock):
271
+ frame.node.doc = doc_text
272
+
208
273
  for file_lineno, raw_line in enumerate(lines, 1):
209
274
  line = raw_line.rstrip()
210
275
 
276
+ # --- Docstring collection for SCRIPT blocks ---
277
+ # Comments immediately following BEGIN SCRIPT are captured as the
278
+ # docstring. A blank line terminates the doc.
279
+ if block_stack and block_stack[-1].collecting_doc and not in_block_comment:
280
+ frame = block_stack[-1]
281
+ if not line:
282
+ # Blank line terminates doc collection
283
+ _stop_doc_collection()
284
+ _flush_line_comments()
285
+ continue
286
+ # Check for block comment opening
287
+ stripped = line.lstrip()
288
+ if stripped.startswith("/*"):
289
+ # Collect block comment as doc lines
290
+ comment_body = stripped[2:]
291
+ if comment_body.rstrip().endswith("*/"):
292
+ # Single-line block comment: /* text */
293
+ comment_body = comment_body.rstrip()[:-2].strip()
294
+ if comment_body:
295
+ frame.doc_lines.append(comment_body)
296
+ continue
297
+ # Multi-line block comment — collect until */
298
+ if comment_body.strip():
299
+ frame.doc_lines.append(comment_body.rstrip())
300
+ # Let the block comment tracker handle the rest, but mark
301
+ # that we're collecting doc inside a block comment.
302
+ in_block_comment = True
303
+ block_comment_lines = [line]
304
+ block_comment_start = file_lineno
305
+ sql_accum_at_block_comment = False
306
+ # We'll handle the doc finalization when */ is found below.
307
+ # For now, mark doc as still collecting.
308
+ continue
309
+ # Check for single-line comment (not metacommand)
310
+ metacommand_match_doc = _EXEC_LINE_RX.match(line)
311
+ if not metacommand_match_doc and _COMMENT_LINE_RX.match(line):
312
+ # Strip -- prefix and optional leading space
313
+ text = stripped
314
+ if text.startswith("--"):
315
+ text = text[2:]
316
+ if text.startswith(" "):
317
+ text = text[1:]
318
+ frame.doc_lines.append(text.rstrip())
319
+ continue
320
+ # Non-comment line (metacommand or SQL) — stop doc collection
321
+ _stop_doc_collection()
322
+ # Fall through to normal processing
323
+
211
324
  # --- Block comment tracking ---
212
325
  if not line:
213
326
  _flush_line_comments()
@@ -217,14 +330,38 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
217
330
  block_comment_lines.append(line)
218
331
  if len(line) > 1 and line.rstrip().endswith("*/"):
219
332
  in_block_comment = False
220
- _flush_sql(file_lineno)
221
- _current_body().append(
222
- Comment(
223
- span=SourceSpan(source_name, block_comment_start, file_lineno),
224
- text="\n".join(block_comment_lines),
225
- ),
226
- )
333
+ comment_text = "\n".join(block_comment_lines)
334
+ # If we were collecting doc lines when the block comment opened,
335
+ # feed the content into the docstring instead of creating a node.
336
+ if block_stack and block_stack[-1].collecting_doc:
337
+ frame = block_stack[-1]
338
+ # Extract text between /* and */, stripping delimiters
339
+ for bc_line in block_comment_lines:
340
+ stripped_bc = bc_line.strip()
341
+ if stripped_bc.startswith("/*"):
342
+ stripped_bc = stripped_bc[2:]
343
+ if stripped_bc.endswith("*/"):
344
+ stripped_bc = stripped_bc[:-2]
345
+ stripped_bc = stripped_bc.strip()
346
+ if stripped_bc:
347
+ frame.doc_lines.append(stripped_bc)
348
+ block_comment_lines = []
349
+ sql_accum_at_block_comment = False
350
+ continue
351
+ if sql_accum_at_block_comment:
352
+ # Block comment started inside a SQL statement — fold it
353
+ # back into sql_accum so the statement isn't split.
354
+ sql_accum += "\n" + comment_text
355
+ else:
356
+ _flush_sql(file_lineno)
357
+ _current_body().append(
358
+ Comment(
359
+ span=SourceSpan(source_name, block_comment_start, file_lineno),
360
+ text=comment_text,
361
+ ),
362
+ )
227
363
  block_comment_lines = []
364
+ sql_accum_at_block_comment = False
228
365
  continue
229
366
 
230
367
  # --- Single-line comment classification ---
@@ -232,12 +369,17 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
232
369
  comment_match = _COMMENT_LINE_RX.match(line)
233
370
 
234
371
  if comment_match and not metacommand_match and not in_sql_block:
235
- # Accumulate consecutive -- comment lines into a single Comment node.
236
- # Inside a BEGIN SQL block, comments are part of the SQL text (not emitted separately).
237
- _flush_sql(file_lineno)
238
- if not line_comment_lines:
239
- line_comment_start = file_lineno
240
- line_comment_lines.append(line.rstrip())
372
+ if sql_accum:
373
+ # Inside a multi-line SQL statement keep the comment as part
374
+ # of the SQL text so that commented-out columns, WHERE clauses,
375
+ # etc. don't split the statement.
376
+ sql_accum += "\n" + line.rstrip()
377
+ else:
378
+ # Standalone comment (not inside a SQL statement) — accumulate
379
+ # consecutive -- lines into a single Comment node.
380
+ if not line_comment_lines:
381
+ line_comment_start = file_lineno
382
+ line_comment_lines.append(line.rstrip())
241
383
  continue
242
384
 
243
385
  # Non-comment line — flush any accumulated -- comment group before proceeding.
@@ -248,17 +390,24 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
248
390
  if len(stripped) > 1 and stripped.startswith("/*"):
249
391
  block_comment_start = file_lineno
250
392
  block_comment_lines = [line]
393
+ # Remember whether we were inside a SQL statement when the block
394
+ # comment started, so we can fold it back on close.
395
+ sql_accum_at_block_comment = bool(sql_accum)
251
396
  in_block_comment = True
252
397
  if stripped.endswith("*/"):
253
398
  in_block_comment = False
254
- _flush_sql(file_lineno)
255
- _current_body().append(
256
- Comment(
257
- span=SourceSpan(source_name, file_lineno),
258
- text=line,
259
- ),
260
- )
399
+ if sql_accum_at_block_comment:
400
+ sql_accum += "\n" + line.rstrip()
401
+ else:
402
+ _flush_sql(file_lineno)
403
+ _current_body().append(
404
+ Comment(
405
+ span=SourceSpan(source_name, file_lineno),
406
+ text=line,
407
+ ),
408
+ )
261
409
  block_comment_lines = []
410
+ sql_accum_at_block_comment = False
262
411
  continue
263
412
 
264
413
  # --- Metacommand handling ---
@@ -312,7 +461,7 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
312
461
  if m:
313
462
  name = m.group("name").lower()
314
463
  paramexpr = m.group("paramexpr")
315
- param_names = None
464
+ param_defs = None
316
465
  if paramexpr:
317
466
  wp = _WITH_PARAMS_RX.match(paramexpr)
318
467
  if not wp:
@@ -321,13 +470,13 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
321
470
  command_text=line,
322
471
  other_msg=f"Invalid BEGIN SCRIPT metacommand on line {file_lineno} of file {source_name}.",
323
472
  )
324
- param_names = re.findall(r"\w+", wp.group("params"))
473
+ param_defs = _parse_param_defs(wp.group("params"), file_lineno, source_name)
325
474
  block_stack.append(
326
475
  _BlockFrame(
327
476
  ScriptBlock(
328
477
  span=SourceSpan(source_name, file_lineno),
329
478
  name=name,
330
- param_names=param_names,
479
+ param_defs=param_defs,
331
480
  ),
332
481
  kind="script",
333
482
  start_line=file_lineno,
execsql/utils/fileio.py CHANGED
@@ -284,7 +284,15 @@ class FileWriter(multiprocessing.Process):
284
284
  self.active = False
285
285
  self.close_all()
286
286
 
287
- def run(self) -> None:
287
+ def run(self) -> None: # pragma: no cover – runs in a subprocess
288
+ # Ignore SIGINT in the child process — the parent owns Ctrl+C handling
289
+ # and will shut us down via CMD_SHUTDOWN on the queue. Without this,
290
+ # KeyboardInterrupt races through queue.get() and close_all(), producing
291
+ # ugly tracebacks on stderr.
292
+ import signal
293
+
294
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
295
+
288
296
  # Messages in the input queue consist of a 2-tuple, of which the first element
289
297
  # is a command and the second is a tuple of arguments for the function indicated
290
298
  # by that command.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.16.7
3
+ Version: 2.16.12
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
@@ -3,7 +3,7 @@ execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
3
3
  execsql/api.py,sha256=0D2Rl329fvDXERNrJBUzoWEt_VxqCHwRgaGzrq04rVs,19709
4
4
  execsql/config.py,sha256=6icjr8PKenUGfFF6lciSclvejjDzY8GTW1OZ1-IZt-Y,29480
5
5
  execsql/exceptions.py,sha256=j8hykBiof9H3Za9hwLIbDcVB2Xn65ODXXplp1jkvdgM,8453
6
- execsql/format.py,sha256=IsCM5T1iBC54CkBJNfO55ycJk-aKRZJjtnwXk0jepTA,12723
6
+ execsql/format.py,sha256=RZVxfIHduF6AU2c_O6k93I0kxTUa8gfUn6bRbcgxHYM,12762
7
7
  execsql/models.py,sha256=kCTUQg9-vReM6WNFfB_ZrEppuOW5u1uMBQThSkfPC0o,13264
8
8
  execsql/parser.py,sha256=P3ea8k7T_XLMrbhpFNZXwytdShrY302MKnhosqza1lo,15493
9
9
  execsql/plugins.py,sha256=2voLwT6eFap6BCBoZYndNNC_bMEJO1f_aP6xQTVXwYI,12815
@@ -29,7 +29,7 @@ execsql/db/postgres.py,sha256=-GpaA9fi6_vdIDTGyRGxNRaPYAAkEB_aCb-EyLFShsM,21120
29
29
  execsql/db/sqlite.py,sha256=or2JDDt_MyBTAtSXBVhNTcojE9jCE4b98tq39sNOyUg,10401
30
30
  execsql/db/sqlserver.py,sha256=d8PwfNumt-Spit_0llafqQ900suv3CZMbrRsHLU1iB4,7683
31
31
  execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
32
- execsql/debug/repl.py,sha256=rlddBAg7lelHYmeyeXhDqkK8XkKF2t2dVv4nsEMvleU,18247
32
+ execsql/debug/repl.py,sha256=TlIw6XrxwP3QUxuOaQZKp9vbJI55kEfiMtRNMxQRZo8,20747
33
33
  execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
34
34
  execsql/exporters/base.py,sha256=Uhq0PBz8N_pJ7WlIN9225wY-HyxfzWSb9YabHXicBA8,6387
35
35
  execsql/exporters/delimited.py,sha256=Po9RV4UwBLOuRZmPFJk5CErpFvTdLCy--nFJJffZgxM,32383
@@ -69,8 +69,8 @@ execsql/metacommands/conditions.py,sha256=B6fBumkqoPO4wcQbw_ypYITaSnzPemAA1g5GrN
69
69
  execsql/metacommands/connect.py,sha256=Wnlp5PeeaNDaVlaWjCetvarTgQIwIeMPYe8cyslPYeA,14969
70
70
  execsql/metacommands/control.py,sha256=PlTAq34OkcmnOsPm3bZxF4mg1MaNBpENa8wzIKVEY10,8302
71
71
  execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,11370
72
- execsql/metacommands/debug.py,sha256=IfyvGLYclb5i6hzOcIQN_N0gqi5xQhitY_Z5j3CmmKU,8264
73
- execsql/metacommands/dispatch.py,sha256=rY259pp5Bcq1q0N86NvxFgmFyOhYrNtgByzrNjocipI,87083
72
+ execsql/metacommands/debug.py,sha256=3QVm0N5uc7mcZco36kqlDq8tiWW0sdo8E8BoQZkE_DI,11784
73
+ execsql/metacommands/dispatch.py,sha256=Gao7rwiBFdghmYKcbWVm21Va30evobQuoghLY_FOZto,87602
74
74
  execsql/metacommands/io.py,sha256=vlGBje5sgnqeilooMdhJDgSRIhysHy5_7LrKtik9Xjs,3011
75
75
  execsql/metacommands/io_export.py,sha256=J2FnXA6452FIGStv6vbZXORmA-Ges70na0W7YwpMESA,13253
76
76
  execsql/metacommands/io_fileops.py,sha256=QKFj-94W32zxJiDcMpRsaohSCdtCRPIOG10K0WPigFk,9616
@@ -81,42 +81,42 @@ execsql/metacommands/script_ext.py,sha256=sw4YKUQl0SRlVlmhIoGbMokOo_hVqh1EcTuYCN
81
81
  execsql/metacommands/system.py,sha256=azRbv_P8l0t8BkDM9bmAUkhpnLSLHSCcmByqs-a3FxQ,7352
82
82
  execsql/metacommands/upsert.py,sha256=XE_P3mjaUkqT-LR4_28n2NbXwdjSgAGXJyZmKZC4Oy4,21136
83
83
  execsql/script/__init__.py,sha256=3WaBklMVIWjtCsYQ-BVo9UAVEIATOgeGsuyv21YKnxo,3969
84
- execsql/script/ast.py,sha256=yFw9ZvLLDNNSmSgy7cwK2fXLatpp2CKYAAyOneJoKVc,18836
84
+ execsql/script/ast.py,sha256=SLn1fp0QDsqvJegNeIuyKV9f1BFnkEdzRota5EaNTfo,20053
85
85
  execsql/script/control.py,sha256=s-1eZdGARM6H1FwZ6VDdO_f50j7bvvRtTHesfUm9tbc,6144
86
86
  execsql/script/engine.py,sha256=EhuVBniOrFkzAW4I3NIZLt3INHTZJvlYoF7B99rZBLI,29391
87
- execsql/script/executor.py,sha256=SeY7Xiky1hBrNG8Z-MV6uhLhayvVFByUH02N5YZbyHA,36546
88
- execsql/script/parser.py,sha256=nAyLaDP-nL6C-chvlaSu0Oqh8ACcCCZjFacv0Fhyp58,24983
87
+ execsql/script/executor.py,sha256=T-pGa3o5S0oX1ySB5zztzRoJHYRZI58CKw3qcFz_95Q,37602
88
+ execsql/script/parser.py,sha256=huznh4RnW6Y_yJyIIoMG5McUS2T9o3kfOW37CXPR5dk,31689
89
89
  execsql/script/variables.py,sha256=ZSBGQUsoii6w3dLDVY9xxoPIV6wY0sAV_BNIQ6pgQAE,14328
90
90
  execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
91
91
  execsql/utils/auth.py,sha256=onXzNkNZQZxGC5w7eey06sjvAIAX_Lf9g7nUJtcsel0,7009
92
92
  execsql/utils/crypto.py,sha256=KlT6lPk-v0-qQQfAMfWvT9Q6kRdCD3_5JVaLajNocKM,3051
93
93
  execsql/utils/datetime.py,sha256=rMCXAbvj6bxKCYzC97vrludO6PU5DYQ39buZ0smDC5A,3573
94
94
  execsql/utils/errors.py,sha256=I3xiPyDYS8Ftv7Y8P0uqzrTKlCN1dTXe1pMKKFFjmoA,8651
95
- execsql/utils/fileio.py,sha256=3HDm6QFGeXwFMmCsbea4H5Ub6u2Jrx-oNsWA3Q5aytQ,23860
95
+ execsql/utils/fileio.py,sha256=x0kl2oLydCIGbhRCAJDHFknUyGcbKEOnV_M4lWcFmTk,24259
96
96
  execsql/utils/gui.py,sha256=h_7V2zYfp0l6C3Ft8QtUJAPgxCUrFkgIqwimz5s1bbQ,22821
97
97
  execsql/utils/mail.py,sha256=4QXwdPgMh6vQBKLtkdH7IDh3JBczumc3l5up2AUSOZU,5483
98
98
  execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
99
99
  execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
100
100
  execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
101
101
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
102
- execsql2-2.16.7.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
103
- execsql2-2.16.7.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
104
- execsql2-2.16.7.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
105
- execsql2-2.16.7.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
106
- execsql2-2.16.7.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
107
- execsql2-2.16.7.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
108
- execsql2-2.16.7.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
109
- execsql2-2.16.7.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
110
- execsql2-2.16.7.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
111
- execsql2-2.16.7.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
112
- execsql2-2.16.7.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
113
- execsql2-2.16.7.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
114
- execsql2-2.16.7.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
115
- execsql2-2.16.7.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
116
- execsql2-2.16.7.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
117
- execsql2-2.16.7.dist-info/METADATA,sha256=oHrJqm7vLZkhceyfwOT40zUVOIZs_kSp_0F2UGXgblw,20920
118
- execsql2-2.16.7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
119
- execsql2-2.16.7.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
120
- execsql2-2.16.7.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
121
- execsql2-2.16.7.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
122
- execsql2-2.16.7.dist-info/RECORD,,
102
+ execsql2-2.16.12.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
103
+ execsql2-2.16.12.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
104
+ execsql2-2.16.12.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
105
+ execsql2-2.16.12.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
106
+ execsql2-2.16.12.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
107
+ execsql2-2.16.12.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
108
+ execsql2-2.16.12.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
109
+ execsql2-2.16.12.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
110
+ execsql2-2.16.12.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
111
+ execsql2-2.16.12.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
112
+ execsql2-2.16.12.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
113
+ execsql2-2.16.12.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
114
+ execsql2-2.16.12.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
115
+ execsql2-2.16.12.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
116
+ execsql2-2.16.12.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
117
+ execsql2-2.16.12.dist-info/METADATA,sha256=Op2AjS7Au3YMFnJ8rX0eE3DJm4SZcRVCzoCIhE3cDEo,20921
118
+ execsql2-2.16.12.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
119
+ execsql2-2.16.12.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
120
+ execsql2-2.16.12.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
121
+ execsql2-2.16.12.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
122
+ execsql2-2.16.12.dist-info/RECORD,,