execsql2 2.18.1__py3-none-any.whl → 2.19.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.
Files changed (37) hide show
  1. execsql/cli/run.py +46 -17
  2. execsql/data/execsql.conf.template +34 -2
  3. execsql/db/access.py +0 -6
  4. execsql/db/base.py +0 -13
  5. execsql/db/mysql.py +0 -6
  6. execsql/db/oracle.py +0 -6
  7. execsql/db/sqlserver.py +0 -6
  8. execsql/debug/repl.py +117 -35
  9. execsql/exporters/feather.py +10 -9
  10. execsql/format.py +23 -4
  11. execsql/importers/base.py +3 -4
  12. execsql/importers/xls.py +6 -1
  13. execsql/metacommands/__init__.py +2 -2
  14. execsql/metacommands/data.py +1 -0
  15. execsql/script/executor.py +2 -6
  16. execsql/script/parser.py +49 -12
  17. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/example_config_prompt.sql +1 -1
  18. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/execsql.conf +34 -2
  19. {execsql2-2.18.1.dist-info → execsql2-2.19.0.dist-info}/METADATA +13 -4
  20. {execsql2-2.18.1.dist-info → execsql2-2.19.0.dist-info}/RECORD +37 -37
  21. {execsql2-2.18.1.dist-info → execsql2-2.19.0.dist-info}/WHEEL +1 -1
  22. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/README.md +0 -0
  23. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  24. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  25. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  26. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  27. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  28. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  29. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  30. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  31. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/script_template.sql +0 -0
  32. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  33. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  34. {execsql2-2.18.1.data → execsql2-2.19.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  35. {execsql2-2.18.1.dist-info → execsql2-2.19.0.dist-info}/entry_points.txt +0 -0
  36. {execsql2-2.18.1.dist-info → execsql2-2.19.0.dist-info}/licenses/LICENSE.txt +0 -0
  37. {execsql2-2.18.1.dist-info → execsql2-2.19.0.dist-info}/licenses/NOTICE +0 -0
execsql/cli/run.py CHANGED
@@ -65,12 +65,21 @@ def _print_dry_run(tree: object) -> None:
65
65
  else:
66
66
  ctype = "METACMD"
67
67
  raw = "-- !x! " + node.command
68
- source_info = f"[dim]{node.span.file}:{node.span.start_line}[/dim]"
68
+ file_loc = f"{node.span.file}:{node.span.start_line}"
69
69
  try:
70
70
  expanded = substitute_vars(raw)
71
71
  except Exception:
72
72
  expanded = raw
73
- _console.print(f" [dim]{i:>4}[/dim] [bold green]{ctype}[/bold green] {source_info} {expanded}")
73
+ lines = expanded.splitlines() or [""]
74
+ first, *continuations = lines
75
+ _console.print(
76
+ f" [dim]{i:>4}[/dim] [bold green]{ctype}[/bold green] [dim]{file_loc}[/dim] {first}",
77
+ )
78
+ if continuations:
79
+ prefix_width = 2 + 4 + 2 + 7 + 2 + len(file_loc) + 2
80
+ pad = " " * prefix_width
81
+ for cont in continuations:
82
+ _console.print(f"{pad}{cont}")
74
83
 
75
84
 
76
85
  # ---------------------------------------------------------------------------
@@ -411,6 +420,12 @@ def _route_positionals(
411
420
  ping: bool,
412
421
  ) -> None:
413
422
  """Apply remaining positional CLI arguments to *conf* as server/db/db_file."""
423
+ # When --ping is set, the script-file positional is ignored (ping has no
424
+ # script). Users who add --ping to an existing invocation often leave the
425
+ # script in place — drop it from positional routing so the connection args
426
+ # behind it are interpreted correctly.
427
+ if ping and positional and Path(positional[0]).is_file() and positional[0].lower().endswith(".sql"):
428
+ positional = positional[1:]
414
429
  off = 0 if (command is not None or ping) else 1
415
430
  if len(positional) == off + 1:
416
431
  if conf.db_type in ("a", "l", "k"):
@@ -648,6 +663,33 @@ def _run(
648
663
 
649
664
  _state.output = WriteHooks()
650
665
 
666
+ # ------------------------------------------------------------------
667
+ # Early exits — modes that touch nothing besides parsing/connecting.
668
+ # Keep these before FileWriter / log / atexit so --dry-run and --ping
669
+ # honor their "no side effects" contracts.
670
+ # ------------------------------------------------------------------
671
+ if dry_run:
672
+ # Seed -a assign-args so substitute_vars in the dry-run print can
673
+ # expand them; the normal path does this inside _setup_logging,
674
+ # which we are skipping.
675
+ if sub_vars:
676
+ for n, repl in enumerate(sub_vars):
677
+ _state.subvars.add_substitution(f"$ARG_{n + 1}", repl)
678
+ _ast_tree = _load_script(command, script_name, conf.script_encoding)
679
+ _print_dry_run(_ast_tree)
680
+ raise SystemExit(0)
681
+
682
+ if ping:
683
+ if conf.server is None and conf.db is None and conf.db_file is None:
684
+ from execsql.utils.errors import fatal_error
685
+
686
+ fatal_error(
687
+ "Database not specified for --ping in configuration files or command-line arguments.",
688
+ )
689
+ db = _connect_initial_db(conf)
690
+ _state.dbs.add("initial", db)
691
+ _ping_db(db) # raises SystemExit
692
+
651
693
  import execsql.utils.fileio as _fileio
652
694
 
653
695
  if _state.filewriter is None or not _state.filewriter.is_alive():
@@ -696,16 +738,9 @@ def _run(
696
738
  )
697
739
 
698
740
  # ------------------------------------------------------------------
699
- # Load the SQL script (skipped in --ping mode)
741
+ # Load the SQL script (--dry-run / --ping already exited above)
700
742
  # ------------------------------------------------------------------
701
- _ast_tree = None if ping else _load_script(command, script_name, conf.script_encoding)
702
-
703
- # ------------------------------------------------------------------
704
- # Dry-run: print command list and exit without connecting to DB
705
- # ------------------------------------------------------------------
706
- if dry_run:
707
- _print_dry_run(_ast_tree)
708
- raise SystemExit(0)
743
+ _ast_tree = _load_script(command, script_name, conf.script_encoding)
709
744
 
710
745
  # ------------------------------------------------------------------
711
746
  # NOTE: --lint is handled as an early exit in cli/__init__.py (AST
@@ -741,12 +776,6 @@ def _run(
741
776
  _state.subvars.add_substitution("$DB_SERVER", db.server_name)
742
777
  _state.subvars.add_substitution("$SYSTEM_CMD_EXIT_STATUS", "0")
743
778
 
744
- # ------------------------------------------------------------------
745
- # --ping: report connection details and exit (no script executed)
746
- # ------------------------------------------------------------------
747
- if ping:
748
- _ping_db(db) # raises SystemExit(0) on success
749
-
750
779
  # ------------------------------------------------------------------
751
780
  # Execute the script
752
781
  # ------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  # execsql.conf — Configuration file for execsql
2
2
  #
3
- # Documentation: https://execsql2.readthedocs.io/reference/configuration/
3
+ # Documentation: https://execsql2.readthedocs.io/en/latest/reference/configuration/
4
4
  #
5
5
  # This file uses INI format. Section names are case-sensitive (lowercase).
6
6
  # Property names are not case-sensitive. Lines starting with # are comments.
@@ -263,7 +263,7 @@
263
263
  #message_css=
264
264
 
265
265
  # Obfuscated password (XOR, not cryptographically secure — use keyring instead).
266
- # See: https://execsql2.readthedocs.io/reference/security/#credentials
266
+ # See: https://execsql2.readthedocs.io/en/latest/reference/security/#credentials
267
267
  #enc_password=
268
268
 
269
269
 
@@ -279,6 +279,38 @@
279
279
  # Values: Yes or No. Default: Yes.
280
280
  #allow_system_cmd=Yes
281
281
 
282
+ # Whether to allow the RM_FILE metacommand (which deletes a file).
283
+ # Set to No to prevent scripts from deleting files.
284
+ # Also controllable via --no-rm-file CLI flag.
285
+ # Values: Yes or No. Default: Yes.
286
+ #allow_rm_file=Yes
287
+
288
+ # Whether to allow the SERVE metacommand (which opens an HTTP server on
289
+ # a local port to serve a single file). Set to No to disable.
290
+ # Also controllable via --no-serve CLI flag.
291
+ # Values: Yes or No. Default: Yes.
292
+ #allow_serve=Yes
293
+
294
+ # Root directory under which INCLUDE / EXECUTE SCRIPT targets must
295
+ # resolve. When set, attempts to include files outside this root via
296
+ # ../, absolute paths, drive letters, or UNC paths are rejected with
297
+ # an error. Default: no containment (any readable path is permitted).
298
+ #include_root=
299
+
300
+ # Root directory under which SERVE targets must resolve. Same
301
+ # containment semantics as include_root. Default: no containment.
302
+ #serve_root=
303
+
304
+ # Root directory under which Jinja2 / string.Template loader paths
305
+ # must resolve. Same containment semantics as include_root.
306
+ # Default: no containment.
307
+ #template_root=
308
+
309
+ # Maximum byte size of any single substitution-variable expansion,
310
+ # enforced by the substitute_vars() engine to defeat exponential-
311
+ # expansion bombs. Default: 10 MB (10485760).
312
+ #max_substitution_bytes=10485760
313
+
282
314
  # Whether to log all data variable assignments.
283
315
  # Values: Yes or No. Default: Yes.
284
316
  #log_datavars=Yes
execsql/db/access.py CHANGED
@@ -88,12 +88,6 @@ class AccessDatabase(Database):
88
88
  def __repr__(self) -> str:
89
89
  return f"AccessDatabase({self.db_name}, {self.encoding})"
90
90
 
91
- def auto_commits_ddl(self) -> bool:
92
- """MS Access (via Jet/ACE on pyodbc) implicitly commits DDL —
93
- ``rollback()`` is a silent no-op for any transaction whose
94
- boundary the DDL crossed."""
95
- return True
96
-
97
91
  def open_db(self) -> None:
98
92
  """Open an ODBC connection to the Access database."""
99
93
  # Open an ODBC connection.
execsql/db/base.py CHANGED
@@ -253,19 +253,6 @@ class Database(ABC):
253
253
  """
254
254
  return False
255
255
 
256
- def auto_commits_ddl(self) -> bool:
257
- """Return True if this adapter's driver implicitly commits DDL.
258
-
259
- Oracle, MySQL, SQL Server, and MS Access all auto-commit DDL —
260
- ``rollback()`` is a silent no-op for any transaction whose
261
- boundary the DDL crossed. Callers that wrap DDL inside an
262
- explicit ``BEGIN BATCH … END BATCH`` block on these adapters
263
- get weaker rollback guarantees than on PostgreSQL / SQLite, and
264
- should be aware of the asymmetry. See
265
- ``docs/about/divergence.md`` for the full per-DBMS matrix.
266
- """
267
- return False
268
-
269
256
  def schema_qualified_table_name(self, schema_name: str | None, table_name: str) -> str:
270
257
  """Return the quoted, optionally schema-qualified form of *table_name*."""
271
258
  table_name = self.type.quoted(table_name)
execsql/db/mysql.py CHANGED
@@ -73,12 +73,6 @@ class MySQLDatabase(Database):
73
73
  f"{self.need_passwd!r}, {self.port!r}, {self.encoding!r})"
74
74
  )
75
75
 
76
- def auto_commits_ddl(self) -> bool:
77
- """MySQL / MariaDB implicitly commit DDL — ``rollback()`` is a
78
- silent no-op for any transaction whose boundary the DDL
79
- crossed. See ``docs/about/divergence.md``."""
80
- return True
81
-
82
76
  def quote_identifier(self, identifier: str) -> str:
83
77
  """MySQL / MariaDB native identifier quoting uses backticks.
84
78
 
execsql/db/oracle.py CHANGED
@@ -61,12 +61,6 @@ class OracleDatabase(Database):
61
61
  f"{self.need_passwd!r}, {self.port!r}, {self.encoding!r})"
62
62
  )
63
63
 
64
- def auto_commits_ddl(self) -> bool:
65
- """Oracle implicitly commits DDL — ``rollback()`` is a silent
66
- no-op for any transaction whose boundary the DDL crossed.
67
- See ``docs/about/divergence.md`` for the per-DBMS matrix."""
68
- return True
69
-
70
64
  def open_db(self) -> None:
71
65
  """Open a connection to the Oracle database."""
72
66
  import cx_Oracle
execsql/db/sqlserver.py CHANGED
@@ -58,12 +58,6 @@ class SqlServerDatabase(Database):
58
58
  f"{self.need_passwd!r}, {self.port!r}, {self.encoding!r})"
59
59
  )
60
60
 
61
- def auto_commits_ddl(self) -> bool:
62
- """SQL Server implicitly commits DDL on Microsoft's pyodbc
63
- driver in autocommit mode — ``rollback()`` is a silent no-op
64
- for any transaction whose boundary the DDL crossed."""
65
- return True
66
-
67
61
  def quote_identifier(self, identifier: str) -> str:
68
62
  """SQL Server native identifier quoting uses square brackets.
69
63
 
execsql/debug/repl.py CHANGED
@@ -12,10 +12,26 @@ The REPL allows the user to:
12
12
  - Step through the script one statement at a time.
13
13
  - Resume or abort execution.
14
14
 
15
- All REPL commands are dot-prefixed (``.continue``, ``.vars``, ``.next``)
16
- to avoid ambiguity with variable names and SQL. Anything not starting
17
- with ``.`` is treated as either a variable lookup (if it matches a known
18
- variable) or SQL (if it ends with ``;``).
15
+ Dispatch is two-way (psql-style): input starting with ``.`` is a REPL
16
+ command (``.continue``, ``.vars [VAR]``, ``.next``, etc.), everything
17
+ else is SQL. Multi-line SQL is supported: any non-``.`` input opens a
18
+ buffer whose continuation prompt is `` ... > ``, accumulating
19
+ until a line ends with ``;``. ``.cancel`` (or Ctrl-C / EOF) discards
20
+ the partial buffer.
21
+
22
+ Variable lookup is explicit — ``.vars LOGFILE`` prints one variable;
23
+ ``.vars`` lists them all. There is no bare-identifier lookup, so any
24
+ SQL keyword you type starts a buffer the moment you press Enter.
25
+
26
+ The trailing ``;`` is the SQL terminator both within one line and across
27
+ multiple lines — the REPL has no read-only mode and DDL on most adapters
28
+ is irreversible, so requiring ``;`` is a small intent gate against
29
+ accidental DML/DDL on mistyped input. Use SQL ``BEGIN; … ROLLBACK;`` to
30
+ bracket exploratory DML if you need recoverability.
31
+
32
+ Errors raised by any REPL helper — bad SQL, malformed dot-commands, etc. —
33
+ are caught at the loop level so the session re-prompts instead of escaping
34
+ through ``x_breakpoint`` and being stamped as a "Metacommand error".
19
35
 
20
36
  In non-interactive environments (CI, piped input, ``sys.stdin.isatty()`` is
21
37
  ``False``) the metacommand is silently skipped so automated pipelines are not
@@ -104,21 +120,21 @@ def _c(code: str, text: str) -> str:
104
120
 
105
121
  _HELP_COMMANDS = [
106
122
  (".continue", ".c", "Resume script execution"),
107
- (".abort", ".q", "Halt the script (exit 1)"),
108
- (".vars", ".v", "List user, system, local, and counter variables"),
109
- (".vars all", ".v all", "Include environment variables (&) in the listing"),
123
+ (".quit", ".q", "Halt the script (exit 1)"),
124
+ (".vars", ".v", "List all execsql substitution variables"),
125
+ (".vars VAR", ".v VAR", "Print the value of a single variable (e.g. .vars logfile)"),
110
126
  (".next", ".n", "Execute the next statement then pause again (step mode)"),
111
127
  (".where", ".w", "Show the current script location and upcoming statement"),
112
128
  (".stack", "", "Show the command-list stack (script name, line, depth)"),
113
129
  (".set VAR VAL", ".s", "Set or update a substitution variable"),
114
130
  (".scripts", "", "List all registered SCRIPT definitions"),
115
131
  (".scripts NAME", "", "Show detail for a specific SCRIPT"),
132
+ (".cancel", "", "Discard a partial multi-line SQL buffer"),
116
133
  (".help", ".h", "Show this help text"),
117
134
  ]
118
135
 
119
136
  _HELP_OTHER = [
120
- ("varname", "Print a variable's value (e.g. logfile, $ARG_1, &HOME)"),
121
- ("SELECT ...;", "Run SQL ending with ';' (expects columns returned, e.g. SELECT)"),
137
+ ("SELECT ...;", "Run SQL multi-line accepted, terminate with ';' to execute"),
122
138
  ]
123
139
 
124
140
  _HELP_CMD_WIDTH = 13 # width of the command column
@@ -216,40 +232,71 @@ def _debug_repl(*, step: bool = False) -> None:
216
232
  _hint_c = _c(_DIM, "'.c'")
217
233
  _write(f" Type {_hint_help} for commands, {_hint_c} to resume.\n\n")
218
234
 
235
+ sql_buffer: list[str] = []
236
+
219
237
  while True:
238
+ prompt = " ... > " if sql_buffer else "execsql debug> "
220
239
  try:
221
- line = input("execsql debug> ").strip()
240
+ line = input(prompt).strip()
222
241
  except EOFError:
242
+ if sql_buffer:
243
+ sql_buffer.clear()
244
+ _write("\n (input discarded)\n")
245
+ continue
223
246
  _write("\n")
224
- return # Ctrl-D → continue
247
+ return
225
248
  except KeyboardInterrupt:
249
+ if sql_buffer:
250
+ sql_buffer.clear()
251
+ _write("\n (input discarded)\n")
252
+ continue
226
253
  _write("\n")
227
- return # Ctrl-C → continue
254
+ return
228
255
 
229
256
  if not line:
230
257
  continue
231
258
 
232
- # Dot-prefixed → REPL command
233
- if line.startswith("."):
234
- cmd = line[1:].strip().lower()
235
- _handle_dot_command(line)
236
- if cmd in ("continue", "c"):
237
- return
238
- if cmd in ("abort", "q", "quit"):
239
- # _handle_dot_command already raised SystemExit, but guard anyway
240
- return
241
- if cmd in ("next", "n"):
242
- return
259
+ try:
260
+ if line.startswith("."):
261
+ cmd = line[1:].strip().lower()
262
+ if cmd == "cancel":
263
+ if sql_buffer:
264
+ sql_buffer.clear()
265
+ _write(" (input discarded)\n")
266
+ continue
267
+ _handle_dot_command(line)
268
+ if cmd in ("continue", "c"):
269
+ return
270
+ if cmd in ("abort", "q", "quit"):
271
+ return
272
+ if cmd in ("next", "n"):
273
+ return
274
+ continue
275
+
276
+ if sql_buffer:
277
+ sql_buffer.append(line)
278
+ joined = " ".join(sql_buffer)
279
+ if joined.rstrip().endswith(";"):
280
+ _run_sql(joined)
281
+ sql_buffer.clear()
282
+ continue
283
+
284
+ if line.rstrip().endswith(";"):
285
+ _run_sql(line)
286
+ continue
287
+
288
+ sql_buffer.append(line)
289
+ except SystemExit:
290
+ raise
291
+ except KeyboardInterrupt:
292
+ sql_buffer.clear()
293
+ _write("\n (interrupted)\n")
243
294
  continue
244
-
245
- # SQL (ends with semicolon)
246
- if line.rstrip().endswith(";"):
247
- _run_sql(line)
295
+ except Exception as exc:
296
+ sql_buffer.clear()
297
+ _write(f" {_c(_RED, 'Error:')} {exc}\n")
248
298
  continue
249
299
 
250
- # Everything else → variable lookup
251
- _print_var(line)
252
-
253
300
 
254
301
  def _handle_dot_command(line: str) -> None:
255
302
  """Dispatch a dot-prefixed REPL command."""
@@ -262,10 +309,14 @@ def _handle_dot_command(line: str) -> None:
262
309
  raise SystemExit(1)
263
310
  elif cmd in ("help", "h"):
264
311
  _write(_format_help())
265
- elif cmd in ("vars all", "v all"):
266
- _print_all_vars(include_env=True)
267
312
  elif cmd in ("vars", "v"):
268
313
  _print_all_vars()
314
+ elif cmd.startswith("vars ") or cmd.startswith("v "):
315
+ rest = cmd.split(None, 1)[1].strip() if " " in cmd else ""
316
+ if rest:
317
+ _print_var(rest)
318
+ else:
319
+ _print_all_vars()
269
320
  elif cmd in ("where", "w"):
270
321
  _print_where()
271
322
  elif cmd == "stack":
@@ -383,7 +434,11 @@ def _print_all_vars(*, include_env: bool = False) -> None:
383
434
  _write(f" {_c(_BOLD, label)}:\n")
384
435
  max_name = max(len(n) for n, _ in group)
385
436
  for name, value in group:
386
- _write(f" {_c(_CYAN, name):<{max_name}} {_c(_DIM, '=')} {value}\n")
437
+ # Pre-compute padding from RAW name length — the format-spec ``:<{max_name}``
438
+ # form measures the colored string, which (when ANSI is on) is ~9 chars
439
+ # longer per wrap and breaks the column alignment.
440
+ pad = " " * (max_name - len(name))
441
+ _write(f" {_c(_CYAN, name)}{pad} {_c(_DIM, '=')} {value}\n")
387
442
 
388
443
  _print_group("User", user_vars)
389
444
  _print_group("System ($)", system_vars)
@@ -477,7 +532,7 @@ def _print_stack() -> None:
477
532
 
478
533
 
479
534
  def _run_sql(sql: str) -> None:
480
- """Execute ad-hoc SQL against the current database and pretty-print the results."""
535
+ """Execute ad-hoc SQL and pretty-print results or affected rowcount."""
481
536
  dbs = _state.dbs
482
537
  if dbs is None:
483
538
  _write(" (no database connection is active)\n")
@@ -486,8 +541,35 @@ def _run_sql(sql: str) -> None:
486
541
  if db is None:
487
542
  _write(" (no database connection is active)\n")
488
543
  return
544
+
489
545
  try:
490
- colnames, rows = db.select_data(sql)
546
+ with db._cursor() as curs:
547
+ try:
548
+ curs.execute(sql)
549
+ except Exception as exc:
550
+ try:
551
+ db.rollback()
552
+ except Exception:
553
+ pass
554
+ _write(f" {_c(_RED, 'SQL error:')} {exc}\n")
555
+ return
556
+
557
+ try:
558
+ _state.subvars.add_substitution("$LAST_ROWCOUNT", curs.rowcount)
559
+ except Exception:
560
+ pass
561
+
562
+ if curs.description is None:
563
+ rowcount = curs.rowcount if curs.rowcount is not None else -1
564
+ if rowcount >= 0:
565
+ row_word = "row" if rowcount == 1 else "rows"
566
+ _write(f" {_c(_DIM, f'({rowcount} {row_word} affected)')}\n")
567
+ else:
568
+ _write(f" {_c(_DIM, '(statement executed)')}\n")
569
+ return
570
+
571
+ colnames = [d[0] for d in curs.description]
572
+ rows = curs.fetchall()
491
573
  except Exception as exc:
492
574
  _write(f" {_c(_RED, 'SQL error:')} {exc}\n")
493
575
  return
@@ -58,20 +58,26 @@ def write_query_to_hdf5(
58
58
  other_msg="The tables Python library must be installed to export data to the HDF5 format.",
59
59
  ) from e
60
60
  try:
61
- hdrs, rows = db.select_rowsource(select_stmt)
61
+ hdrs, row_iter = db.select_rowsource(select_stmt)
62
62
  except ErrInfo:
63
63
  raise
64
64
  except Exception as e:
65
65
  raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
66
+ # Materialize once so DataTable's type inference (below) and the write
67
+ # loop (further below) share the same rows without a second query.
68
+ rows = list(row_iter)
66
69
 
67
70
  def h5type(datatype, size):
68
- if datatype in (_state.DT_Varchar, _state.DT_Text):
71
+ if datatype == _state.DT_Varchar:
69
72
  t = tables.StringCol(size)
70
73
  do_cast = False
71
74
  elif datatype == _state.DT_Text:
72
75
  t = tables.StringCol(_state.conf.hdf5_text_len)
73
76
  do_cast = False
74
- elif datatype in (_state.DT_Integer, _state.DT_Long):
77
+ elif datatype == _state.DT_Long:
78
+ t = tables.Int64Col()
79
+ do_cast = False
80
+ elif datatype == _state.DT_Integer:
75
81
  t = tables.IntCol()
76
82
  do_cast = False
77
83
  elif datatype in (_state.DT_Float, _state.DT_Decimal):
@@ -87,25 +93,20 @@ def write_query_to_hdf5(
87
93
  raise ErrInfo("error", other_msg=f"Invalid data type for export to HDF5: {repr(datatype)}")
88
94
  return t, do_cast
89
95
 
90
- # Create a dictionary of column names with the HDF5 data types
91
96
  tbl_desc = DataTable(hdrs, rows)
92
97
  h5type_dict = {}
93
98
  cast_flags = []
94
- # Iterate over hdrs instead of tbl_desc.cols to preserve column order.
95
99
  for h in hdrs:
96
100
  dt = [col for col in tbl_desc.cols if col.name == h][0].dt
97
- # dt is a tuple of: 0: the column name; 1: the data type class; 2: the maximum length or None if NA; other info.
101
+ # dt is (name, data-type-class, max-length-or-None, ...)
98
102
  h5typ, as_str = h5type(dt[1], dt[2])
99
103
  h5type_dict[h] = h5typ
100
104
  cast_flags.append(as_str)
101
- # Open the HDF5 table
102
105
  filewriter_close(outfile)
103
106
  h5file_mode = "a" if append else "w"
104
107
  h5file = tables.open_file(outfile, mode=h5file_mode)
105
108
  h5grp = h5file.create_group("/", table_name, title=desc)
106
109
  h5tbl = h5file.create_table(h5grp, table_name, h5type_dict)
107
- # Write the data.
108
- hdrs, rows = db.select_rowsource(select_stmt)
109
110
  for datarow in rows:
110
111
  h5row = h5tbl.row
111
112
  for i, h in enumerate(hdrs):
execsql/format.py CHANGED
@@ -16,11 +16,27 @@ import io
16
16
  import re
17
17
  from pathlib import Path
18
18
 
19
- import sqlglot
20
- import sqlglot.errors
21
-
22
19
  __all__ = ["collect_paths", "format_file", "main", "parse_keyword"]
23
20
 
21
+
22
+ _SQLGLOT_MISSING_MSG = (
23
+ "execsql-format requires sqlglot for SQL reformatting.\n"
24
+ " Install with: pip install execsql2[formatter]\n"
25
+ " Or skip SQL reformatting with the --no-sql flag."
26
+ )
27
+
28
+
29
+ def _require_sqlglot():
30
+ """Lazy import of sqlglot, raising ImportError with an install hint if missing."""
31
+ try:
32
+ import sqlglot
33
+ import sqlglot.errors # noqa: F401
34
+
35
+ return sqlglot
36
+ except ImportError as e:
37
+ raise ImportError(_SQLGLOT_MISSING_MSG) from e
38
+
39
+
24
40
  # ---------------------------------------------------------------------------
25
41
  # Constants
26
42
  # ---------------------------------------------------------------------------
@@ -161,6 +177,9 @@ def _sqlglot_format(
161
177
  leading_comma: bool = False,
162
178
  ) -> list[str]:
163
179
  """Format a list of SQL-only lines (no comment-only lines) via sqlglot."""
180
+ sqlglot = _require_sqlglot()
181
+ import sqlglot.errors as sqlglot_errors
182
+
164
183
  text = "\n".join(sql_lines)
165
184
  protected, replacements = _protect_variables(text)
166
185
 
@@ -169,7 +188,7 @@ def _sqlglot_format(
169
188
 
170
189
  try:
171
190
  with contextlib.redirect_stderr(io.StringIO()):
172
- ast = sqlglot.parse(protected, read="postgres", error_level=sqlglot.errors.ErrorLevel.IGNORE)
191
+ ast = sqlglot.parse(protected, read="postgres", error_level=sqlglot_errors.ErrorLevel.IGNORE)
173
192
  statements: list[str] = []
174
193
  for node in ast:
175
194
  if node is None:
execsql/importers/base.py CHANGED
@@ -15,7 +15,6 @@ from typing import Any
15
15
  from execsql.exceptions import ErrInfo
16
16
  from execsql.db.base import Database
17
17
  import execsql.state as _state
18
- from execsql.types import dbt_firebird
19
18
 
20
19
  __all__ = ["import_data_table"]
21
20
 
@@ -84,9 +83,9 @@ def import_data_table(
84
83
  sql = get_ts().create_table(db.type, schemaname, tablename)
85
84
  try:
86
85
  db.execute(sql)
87
- # Don't commit here; commit will be done after populating the table
88
- # ...except for Firebird.
89
- if db.type == dbt_firebird:
86
+ # Most adapters delay commit until after populating the table; adapters
87
+ # that need DDL committed first (Firebird) must do it here.
88
+ if db.needs_explicit_commit_after_ddl():
90
89
  db.conn.commit()
91
90
  except Exception as e:
92
91
  raise ErrInfo(
execsql/importers/xls.py CHANGED
@@ -39,7 +39,12 @@ def xls_data(
39
39
 
40
40
  wbk = XlsFile()
41
41
  elif ext3 == "lsx":
42
- # openpyxl imported lazily
42
+ from execsql.utils.fileio import check_zip_decompression_ratio
43
+
44
+ try:
45
+ check_zip_decompression_ratio(filename)
46
+ except FileNotFoundError:
47
+ pass
43
48
  from execsql.exporters.xls import XlsxFile
44
49
 
45
50
  wbk = XlsxFile()
@@ -2,8 +2,8 @@
2
2
 
3
3
  Importing this module populates a ``MetaCommandList`` (``DISPATCH_TABLE``)
4
4
  with every metacommand regex and its handler function. The dispatch
5
- table is consumed by ``script.MetacommandStmt.run()`` via
6
- ``_state.metacommandlist``.
5
+ table is consumed by :func:`execsql.script.executor._exec_metacommand`
6
+ via ``_state.metacommandlist``.
7
7
 
8
8
  The table itself is built by ``build_dispatch_table()`` in
9
9
  :mod:`execsql.metacommands.dispatch`. Handler functions are organized
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  from execsql.exceptions import ErrInfo
3
4
 
4
5
  """
@@ -83,10 +83,6 @@ _DEFER_RX = re.compile(r"!\{([$@&~#+]?\w+)\}!")
83
83
  _VARLIKE = re.compile(r"!![$@&~#]?\w+!!", re.I)
84
84
 
85
85
 
86
- # Legacy module-level alias — ``_ast_scripts`` is now ``ctx.ast_scripts``
87
- # on the RuntimeContext. Kept as a comment for grep-ability.
88
-
89
-
90
86
  def _stack_localvars(ctx: RuntimeContext) -> SubVarSet | None:
91
87
  """Build the merged ``~`` local + ``#`` param overlay for the current scope.
92
88
 
@@ -210,7 +206,7 @@ def _set_command_vars(ctx: RuntimeContext, source: str, line_no: int) -> None:
210
206
 
211
207
 
212
208
  # ---------------------------------------------------------------------------
213
- # SQL execution (bypasses SqlStmt.run's if_stack check)
209
+ # SQL execution
214
210
  # ---------------------------------------------------------------------------
215
211
 
216
212
 
@@ -264,7 +260,7 @@ def _exec_sql(
264
260
 
265
261
 
266
262
  # ---------------------------------------------------------------------------
267
- # Metacommand execution (bypasses MetacommandStmt.run's if_stack check)
263
+ # Metacommand execution
268
264
  # ---------------------------------------------------------------------------
269
265
 
270
266
 
execsql/script/parser.py CHANGED
@@ -102,6 +102,19 @@ def _strip_quotes(s: str) -> str:
102
102
  return s
103
103
 
104
104
 
105
+ def _unclosed_block_msg(top: _BlockFrame) -> str:
106
+ """Message blaming an unclosed opening block for a wrong-kind END-keyword error."""
107
+ if top.kind == "if":
108
+ return f"IF on line {top.start_line} of {top.source} has no matching ENDIF."
109
+ if top.kind == "loop":
110
+ return f"LOOP on line {top.start_line} of {top.source} has no matching ENDLOOP."
111
+ if top.kind == "batch":
112
+ return f"BEGIN BATCH on line {top.start_line} of {top.source} has no matching END BATCH."
113
+ if top.kind == "script":
114
+ return f"BEGIN SCRIPT on line {top.start_line} of {top.source} has no matching END SCRIPT."
115
+ return f"{top.kind.upper()} on line {top.start_line} of {top.source} is unclosed."
116
+
117
+
105
118
  _EXEC_SCRIPT_RX = re.compile(
106
119
  r"^\s*(?:EXEC(?:UTE)?|RUN)\s+SCRIPT"
107
120
  r"(?P<exists>\s+IF\s+EXISTS)?"
@@ -529,11 +542,17 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
529
542
  end_name = m.group("name")
530
543
  if end_name is not None:
531
544
  end_name = end_name.lower()
532
- if not block_stack or block_stack[-1].kind != "script":
545
+ if not block_stack:
546
+ raise ErrInfo(
547
+ type="cmd",
548
+ command_text=line,
549
+ other_msg=f"END SCRIPT on line {file_lineno} of {source_name} has no matching BEGIN SCRIPT.",
550
+ )
551
+ if block_stack[-1].kind != "script":
533
552
  raise ErrInfo(
534
553
  type="cmd",
535
554
  command_text=line,
536
- other_msg=f"Unmatched END SCRIPT metacommand on line {file_lineno} of file {source_name}.",
555
+ other_msg=_unclosed_block_msg(block_stack[-1]),
537
556
  )
538
557
  frame = block_stack[-1]
539
558
  script_node = frame.node
@@ -595,7 +614,7 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
595
614
  raise ErrInfo(
596
615
  type="cmd",
597
616
  command_text=line,
598
- other_msg=f"ELSEIF without matching IF on line {file_lineno} of {source_name}.",
617
+ other_msg=f"ELSEIF on line {file_lineno} of {source_name} has no matching IF.",
599
618
  )
600
619
  frame = block_stack[-1]
601
620
  frame._in_else = False
@@ -616,7 +635,7 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
616
635
  raise ErrInfo(
617
636
  type="cmd",
618
637
  command_text=line,
619
- other_msg=f"ANDIF without matching IF on line {file_lineno} of {source_name}.",
638
+ other_msg=f"ANDIF on line {file_lineno} of {source_name} has no matching IF.",
620
639
  )
621
640
  modifier = ConditionModifier(
622
641
  kind="AND",
@@ -638,7 +657,7 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
638
657
  raise ErrInfo(
639
658
  type="cmd",
640
659
  command_text=line,
641
- other_msg=f"ORIF without matching IF on line {file_lineno} of {source_name}.",
660
+ other_msg=f"ORIF on line {file_lineno} of {source_name} has no matching IF.",
642
661
  )
643
662
  modifier = ConditionModifier(
644
663
  kind="OR",
@@ -660,7 +679,7 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
660
679
  raise ErrInfo(
661
680
  type="cmd",
662
681
  command_text=line,
663
- other_msg=f"ELSE without matching IF on line {file_lineno} of {source_name}.",
682
+ other_msg=f"ELSE on line {file_lineno} of {source_name} has no matching IF.",
664
683
  )
665
684
  frame = block_stack[-1]
666
685
  frame._in_else = True
@@ -671,11 +690,17 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
671
690
  # -- ENDIF --
672
691
  m = _ENDIF_RX.match(cmd_text)
673
692
  if m:
674
- if not block_stack or block_stack[-1].kind != "if":
693
+ if not block_stack:
675
694
  raise ErrInfo(
676
695
  type="cmd",
677
696
  command_text=line,
678
- other_msg=f"ENDIF without matching IF on line {file_lineno} of {source_name}.",
697
+ other_msg=f"ENDIF on line {file_lineno} of {source_name} has no matching IF.",
698
+ )
699
+ if block_stack[-1].kind != "if":
700
+ raise ErrInfo(
701
+ type="cmd",
702
+ command_text=line,
703
+ other_msg=_unclosed_block_msg(block_stack[-1]),
679
704
  )
680
705
  frame = block_stack.pop()
681
706
  frame.node.span = SourceSpan(source_name, frame.start_line, file_lineno)
@@ -702,11 +727,17 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
702
727
  # -- ENDLOOP --
703
728
  m = _ENDLOOP_RX.match(cmd_text)
704
729
  if m:
705
- if not block_stack or block_stack[-1].kind != "loop":
730
+ if not block_stack:
731
+ raise ErrInfo(
732
+ type="cmd",
733
+ command_text=line,
734
+ other_msg=f"ENDLOOP on line {file_lineno} of {source_name} has no matching LOOP.",
735
+ )
736
+ if block_stack[-1].kind != "loop":
706
737
  raise ErrInfo(
707
738
  type="cmd",
708
739
  command_text=line,
709
- other_msg=f"ENDLOOP without matching LOOP on line {file_lineno} of {source_name}.",
740
+ other_msg=_unclosed_block_msg(block_stack[-1]),
710
741
  )
711
742
  frame = block_stack.pop()
712
743
  frame.node.span = SourceSpan(source_name, frame.start_line, file_lineno)
@@ -729,11 +760,17 @@ def _parse_lines(lines: Iterable[str], source_name: str) -> Script:
729
760
  # -- END BATCH --
730
761
  m = _END_BATCH_RX.match(cmd_text)
731
762
  if m:
732
- if not block_stack or block_stack[-1].kind != "batch":
763
+ if not block_stack:
764
+ raise ErrInfo(
765
+ type="cmd",
766
+ command_text=line,
767
+ other_msg=f"END BATCH on line {file_lineno} of {source_name} has no matching BEGIN BATCH.",
768
+ )
769
+ if block_stack[-1].kind != "batch":
733
770
  raise ErrInfo(
734
771
  type="cmd",
735
772
  command_text=line,
736
- other_msg=f"END BATCH without matching BEGIN BATCH on line {file_lineno} of {source_name}.",
773
+ other_msg=_unclosed_block_msg(block_stack[-1]),
737
774
  )
738
775
  frame = block_stack.pop()
739
776
  frame.node.span = SourceSpan(source_name, frame.start_line, file_lineno)
@@ -122,7 +122,7 @@
122
122
  from configspecs cs inner join configusage cu
123
123
  on cu.sub_var = cs.sub_var
124
124
  where
125
- usage = '!!#usage!!';
125
+ usage = !'!#usage!'!;
126
126
  -- !x! prompt entry_form !!~spectbl!! message "You may change any of the configuration settings below."
127
127
  -- !x! if(sub_defined(~boolean_int)) {config boolean_int !!~boolean_int!!}
128
128
  -- !x! if(sub_defined(~boolean_words)) {config boolean_words !!~boolean_words!!}
@@ -1,6 +1,6 @@
1
1
  # execsql.conf — Configuration file for execsql
2
2
  #
3
- # Documentation: https://execsql2.readthedocs.io/reference/configuration/
3
+ # Documentation: https://execsql2.readthedocs.io/en/latest/reference/configuration/
4
4
  #
5
5
  # This file uses INI format. Section names are case-sensitive (lowercase).
6
6
  # Property names are not case-sensitive. Lines starting with # are comments.
@@ -263,7 +263,7 @@
263
263
  #message_css=
264
264
 
265
265
  # Obfuscated password (XOR, not cryptographically secure — use keyring instead).
266
- # See: https://execsql2.readthedocs.io/reference/security/#credentials
266
+ # See: https://execsql2.readthedocs.io/en/latest/reference/security/#credentials
267
267
  #enc_password=
268
268
 
269
269
 
@@ -279,6 +279,38 @@
279
279
  # Values: Yes or No. Default: Yes.
280
280
  #allow_system_cmd=Yes
281
281
 
282
+ # Whether to allow the RM_FILE metacommand (which deletes a file).
283
+ # Set to No to prevent scripts from deleting files.
284
+ # Also controllable via --no-rm-file CLI flag.
285
+ # Values: Yes or No. Default: Yes.
286
+ #allow_rm_file=Yes
287
+
288
+ # Whether to allow the SERVE metacommand (which opens an HTTP server on
289
+ # a local port to serve a single file). Set to No to disable.
290
+ # Also controllable via --no-serve CLI flag.
291
+ # Values: Yes or No. Default: Yes.
292
+ #allow_serve=Yes
293
+
294
+ # Root directory under which INCLUDE / EXECUTE SCRIPT targets must
295
+ # resolve. When set, attempts to include files outside this root via
296
+ # ../, absolute paths, drive letters, or UNC paths are rejected with
297
+ # an error. Default: no containment (any readable path is permitted).
298
+ #include_root=
299
+
300
+ # Root directory under which SERVE targets must resolve. Same
301
+ # containment semantics as include_root. Default: no containment.
302
+ #serve_root=
303
+
304
+ # Root directory under which Jinja2 / string.Template loader paths
305
+ # must resolve. Same containment semantics as include_root.
306
+ # Default: no containment.
307
+ #template_root=
308
+
309
+ # Maximum byte size of any single substitution-variable expansion,
310
+ # enforced by the substitute_vars() engine to defeat exponential-
311
+ # expansion bombs. Default: 10 MB (10485760).
312
+ #max_substitution_bytes=10485760
313
+
282
314
  # Whether to log all data variable assignments.
283
315
  # Values: Yes or No. Default: Yes.
284
316
  #log_datavars=Yes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.18.1
3
+ Version: 2.19.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
@@ -41,8 +41,7 @@ Classifier: Topic :: Database :: Front-Ends
41
41
  Requires-Python: >=3.10
42
42
  Requires-Dist: python-dateutil>=2.8
43
43
  Requires-Dist: rich>=13.0
44
- Requires-Dist: sqlglot>=25.0
45
- Requires-Dist: textual>=0.47.0
44
+ Requires-Dist: textual>=1.0
46
45
  Requires-Dist: typer>=0.12
47
46
  Provides-Extra: all
48
47
  Requires-Dist: defusedxml; extra == 'all'
@@ -59,6 +58,7 @@ Requires-Dist: psycopg2-binary; extra == 'all'
59
58
  Requires-Dist: pymysql; extra == 'all'
60
59
  Requires-Dist: pyodbc; extra == 'all'
61
60
  Requires-Dist: pyyaml; extra == 'all'
61
+ Requires-Dist: sqlglot>=25.0; extra == 'all'
62
62
  Requires-Dist: tables; extra == 'all'
63
63
  Requires-Dist: tkintermapview>=1.29; extra == 'all'
64
64
  Requires-Dist: xlrd; extra == 'all'
@@ -93,6 +93,7 @@ Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
93
93
  Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
94
94
  Requires-Dist: pyyaml; extra == 'dev'
95
95
  Requires-Dist: ruff>=0.4; extra == 'dev'
96
+ Requires-Dist: sqlglot>=25.0; extra == 'dev'
96
97
  Requires-Dist: tables; extra == 'dev'
97
98
  Requires-Dist: tox-uv>=1.13.1; extra == 'dev'
98
99
  Requires-Dist: twine>=6.1.0; extra == 'dev'
@@ -111,6 +112,8 @@ Requires-Dist: polars; extra == 'formats'
111
112
  Requires-Dist: pyyaml; extra == 'formats'
112
113
  Requires-Dist: tables; extra == 'formats'
113
114
  Requires-Dist: xlrd; extra == 'formats'
115
+ Provides-Extra: formatter
116
+ Requires-Dist: sqlglot>=25.0; extra == 'formatter'
114
117
  Provides-Extra: map
115
118
  Requires-Dist: tkintermapview>=1.29; extra == 'map'
116
119
  Provides-Extra: mssql
@@ -380,14 +383,20 @@ The `PROMPT` metacommand produces a GUI display of the data:
380
383
 
381
384
  # Formatting Scripts
382
385
 
383
- The `execsql-format` command normalizes execsql script files: it uppercases metacommand keywords, corrects block indentation, and optionally reformats SQL via sqlglot. It is installed automatically with the `execsql2` package.
386
+ The `execsql-format` command normalizes execsql script files: it uppercases metacommand keywords, corrects block indentation, and optionally reformats SQL via `sqlglot`. The metacommand / indent / keyword reformatting is built into `execsql2`; SQL reformatting requires the `[formatter]` extra (or pass `--no-sql` to skip it):
384
387
 
385
388
  ```bash
389
+ # Install with the SQL-reformatting extra
390
+ pip install execsql2[formatter]
391
+
386
392
  # Format files in place
387
393
  execsql-format --in-place scripts/
388
394
 
389
395
  # Check formatting without writing (useful in CI)
390
396
  execsql-format --check scripts/
397
+
398
+ # Run the formatter without sqlglot — keyword/indent normalization only
399
+ execsql-format --no-sql --in-place scripts/
391
400
  ```
392
401
 
393
402
  `execsql-format` is also available as a [pre-commit](https://pre-commit.com/) hook:
@@ -3,7 +3,7 @@ execsql/__main__.py,sha256=HdbK-SAhyUmfB6xINY5AzxdMSxGzWSGEG_2dv42Jn64,315
3
3
  execsql/api.py,sha256=ZFTo_XZPhG21w2vxaeS1lS6o5XmF1FUJRIaypgTOjA8,20919
4
4
  execsql/config.py,sha256=OOrCcn9m3CNuXkxVOLp7uMhQikzUS2wh_QVhvIzRqIM,33296
5
5
  execsql/exceptions.py,sha256=EkM5cw2s0D9QCOgS4BU29FEyOnEtCxJ0esPT6l1hT9s,9205
6
- execsql/format.py,sha256=el_gQyMbj4VllToGEcU_61PtxUFotD1hXjrVzEhackM,25507
6
+ execsql/format.py,sha256=HJlWD4kSOvq7lBv99rRF1Cq1FNaMjG-qA3iwSTZtf8Q,26050
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
@@ -14,28 +14,28 @@ execsql/cli/__init__.py,sha256=aJknKKIGxYCXpny0cyHXfqJsJ95dBtlEXXhPASFG8GQ,23114
14
14
  execsql/cli/dsn.py,sha256=svaZtrUXFRL2W5G6FRRiKtR6kehOp7urrVhIx_642Z8,2820
15
15
  execsql/cli/help.py,sha256=ThwdZuMIfLPxLAPpGWwXFY_UfyWvYOCjdlBNK20Vzd8,5718
16
16
  execsql/cli/lint.py,sha256=YqKzFNUhyb_Th69hYgKk1ZZVjCsZfJMIiUGqp06JwNs,17236
17
- execsql/cli/run.py,sha256=QoSHVBfg20n2knPrqf7RFJmcfFpC5aq7NkwX5o6qRnA,36326
17
+ execsql/cli/run.py,sha256=4plvi8ZaGea5fUeOhtS2f4MrVnGTaetvJyz7qX07s0I,37706
18
18
  execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- execsql/data/execsql.conf.template,sha256=1a2g2Vga7s128wcu3ftIFRkHlKKtuvkuOHSD1XuNT7o,9404
19
+ execsql/data/execsql.conf.template,sha256=Sq1Huwb_Uf_lI7zW7m3h11fqU6zjGpGnCU4OVJ_tCe0,10696
20
20
  execsql/db/__init__.py,sha256=jTbuafuKOqYtXFR1wvCOoKK5Lr3l1uErfaIbIr6UywI,1063
21
- execsql/db/access.py,sha256=pqxFzxF5yXBv_UzJEtp5YHNJy3yHqxLYCRzVLV4qolo,19404
22
- execsql/db/base.py,sha256=m0xOGU-GAUU6SR1kF6LUnybaeX3fUgXqsofbzemyM28,35295
21
+ execsql/db/access.py,sha256=GcY5Pq_vXdp8Thzm6aSLg9vfp5koovGiYKXipQtRt50,19167
22
+ execsql/db/base.py,sha256=UUiYSztUen8xCWVCxDkRQeD098Yv1iA3WPl_qo4XjDI,34686
23
23
  execsql/db/dsn.py,sha256=59OzMAuCIfHcdOZNarK9TlDzaBJjhZ1SFFMvyXlH6u8,6086
24
24
  execsql/db/duckdb.py,sha256=79lRzKRhw1Pjfqcrba27S4Oq8a8AbDO_d0XkaNKKPQo,3197
25
25
  execsql/db/factory.py,sha256=YHdgyqQYy16548O3fGyElLC5C7DdIgva4Z29OsDxXjs,5367
26
26
  execsql/db/firebird.py,sha256=p_7RFWhFI7y5ukKCMXeDPE0wjeQ6dpO4IK6uz2dYjrc,9224
27
- execsql/db/mysql.py,sha256=HOFziabEN-ZD97LIfTrfhQB_yXwcCi9ZsqPqt6LFDHk,17964
28
- execsql/db/oracle.py,sha256=vYs1Oqym0LdAV5WQFdJFDesHlsx7RjsOaDyX0UTegPQ,12286
27
+ execsql/db/mysql.py,sha256=gOm1IRzFmxnT4ekGEXiF2yM3jB6r4p38Cmtt1Utj27Y,17712
28
+ execsql/db/oracle.py,sha256=1_odb5xmlm8vjdJdQXz7SHm9dzIbZ5sxP_IxTklH3kA,12018
29
29
  execsql/db/postgres.py,sha256=UNzrXzMniEyT3Z7qjCA_HLEUY0PVr1cJShuhAxtl5l0,21241
30
30
  execsql/db/sqlite.py,sha256=xooU6bvD9Y3frRpnbyesE63r6E1fwEHkkcN1YD_UIUE,11519
31
- execsql/db/sqlserver.py,sha256=6HtjLa__SlVyqbM7S80Wsipe-IFIDnzmYNg8BLvuQJM,9006
31
+ execsql/db/sqlserver.py,sha256=sxtOrcN1pGJQ0x7CctrKIL9XFIaHCDVIVvEvxzEOdzY,8744
32
32
  execsql/debug/__init__.py,sha256=j6EGUR0dHzUhWN1mHHtf1-Lhjq3Sb1V-vmnq2Ztgj1M,178
33
- execsql/debug/repl.py,sha256=d-Pq4wBQU0rcvjXWqDkq_FOxL29vJ7VCP3xoG6TtXpE,22309
33
+ execsql/debug/repl.py,sha256=JObeoEXh15qyk4Q3WQCCqMcBtojlIcu_Xg-ZRDZJi5Q,25491
34
34
  execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
35
35
  execsql/exporters/base.py,sha256=XTPenHl5TbmbZ3cfPYLVFirGNiVps3Kt3AQKFVKy6ss,6408
36
36
  execsql/exporters/delimited.py,sha256=GIEeennL_elvcZgq5oSvgxAKAgwr8ea3o5_M_pLmc4g,32341
37
37
  execsql/exporters/duckdb.py,sha256=R4WbvzBEIK1ptnIc8w6c7jcinG-cuuFYC85_NYCumH0,3146
38
- execsql/exporters/feather.py,sha256=Ie7DCheyAj5y3ktpaMN76Mu3rnTQ0biKcxmaJdcIWvg,4184
38
+ execsql/exporters/feather.py,sha256=ardyidIcuhrqRBJh9ftUZkG3ZCY7NS7eGC-VAnpFIXI,4131
39
39
  execsql/exporters/html.py,sha256=BPTGYODiC5_5zaQsVkZ9QVAl67yfCWFTsjK0D-QOJkM,9717
40
40
  execsql/exporters/json.py,sha256=G9lyJcjgmMvymu_MoVrkSqx2H6JRN7qwA5UEomnPkVQ,4343
41
41
  execsql/exporters/latex.py,sha256=w_B83_5vKPe8uYxCWGdqvxwJeq0mw5zzKYDiAb7dbN0,4503
@@ -59,17 +59,17 @@ execsql/gui/console.py,sha256=pCBUcFGjlKXMkMjztbmt9glP3me9jAKAgQxnmUE38-0,19396
59
59
  execsql/gui/desktop.py,sha256=hdy-1-QxeKXe3GDG1lhxkQHhTjZxH7KNl_mVg5ki1qw,59080
60
60
  execsql/gui/tui.py,sha256=INBCuW6iEEV15P_JrHeviI1JlNCXfoiyPBU-IC5MaLY,65200
61
61
  execsql/importers/__init__.py,sha256=zZwdQxMaValCNqUrVdvaA7XPU3J8NmqVJ4uolNWY2iU,299
62
- execsql/importers/base.py,sha256=FGVz3ntN6xHL99rQixlQj3tAf570K_oU83UtbYE1lJg,4124
62
+ execsql/importers/base.py,sha256=k4ni92H33_-A32dNfxhBd6R5WPwM3o_8dSGABisUlR0,4136
63
63
  execsql/importers/csv.py,sha256=GUVRP294vHlOlF8XNecPEzatUBOOFIqnrEV9cBQkiv0,4849
64
64
  execsql/importers/feather.py,sha256=g2B69d2uv9vmnXcmjFyTVsMP40LYEzFYkhk3gD26mGw,1900
65
65
  execsql/importers/json.py,sha256=fL47h09Zzx4Qw3TTQRQJbdPdD3qVMiU-kK9C7j1IQzg,5534
66
66
  execsql/importers/ods.py,sha256=VsxIkr7opBamB0PbSFmMWt7G11w8lLUH1k_kRwz28zw,2847
67
- execsql/importers/xls.py,sha256=IB-ShXw2zHJUWPUtMCDbkjxYue-hACyR66tKNOZqzSw,3685
68
- execsql/metacommands/__init__.py,sha256=6PE1rXfJzIpHCxIgbeokh6fY2cAe82KR04J6pzawVlE,12993
67
+ execsql/importers/xls.py,sha256=7mGYqXko5_wyvEUz186xSzIx8Eeobn5A6DCVAg4YfTM,3838
68
+ execsql/metacommands/__init__.py,sha256=H_D1Z5kN6zfYtxatejQhQhQV1tlVZ7rcdFZ_9uhXu2Q,13010
69
69
  execsql/metacommands/conditions.py,sha256=5njexsBqSN_MNQdnw9Ra51BcS7ekACHNvBI2MXZF6_g,29995
70
70
  execsql/metacommands/connect.py,sha256=W24gYGmYDXNQyzBTsqWtl9-qbX2FS0v_c4s_OHj97mY,15327
71
71
  execsql/metacommands/control.py,sha256=btF9hP_jzTuTIODPK72CYF0v_oKYpwXpKLATt-Ti2kc,7988
72
- execsql/metacommands/data.py,sha256=u9D6F3ambIqXhEHVmFOI6RDrbmdXQ-FUiqw7aMGo5bQ,12135
72
+ execsql/metacommands/data.py,sha256=PPO6AnF_swFxy9GNrtMsv3YttT5ZrjOMA4QtEORDcso,12136
73
73
  execsql/metacommands/debug.py,sha256=MeVXAob8ItEg2QzuSUkKDaQCEABnH6u0XcAwJzw36CE,13015
74
74
  execsql/metacommands/dispatch.py,sha256=t7x0xWHJA0PeCrYf7jYeSMJgf0yxcZ_xh-_YAtBPHLw,87192
75
75
  execsql/metacommands/io.py,sha256=vlGBje5sgnqeilooMdhJDgSRIhysHy5_7LrKtik9Xjs,3011
@@ -85,8 +85,8 @@ execsql/script/__init__.py,sha256=eGJPBDWj42aaId2lX_quSrqoKrvGwGElIrGDNCyoV1Y,35
85
85
  execsql/script/ast.py,sha256=TQ4_7Lfw1F8_k6ycdvMZdzwNafrZiljSrthVRWUsuIk,20585
86
86
  execsql/script/control.py,sha256=WqLy-HLPqHG3vEzYpKMiIJsD7LpORjyQuUWzFzcGz4w,2327
87
87
  execsql/script/engine.py,sha256=52RmtQJGk4KWqXpZY7jfKeiPojAoULHWaigOcm1azm4,20979
88
- execsql/script/executor.py,sha256=UTQ4k8EjxH5CdKYZ0E_l-0WyQ-i769mUJw6cJmMvSxI,36018
89
- execsql/script/parser.py,sha256=K-mgwuQ729KdmimOpEmb0OBzMyOvX3gxhBKLgr5P4VA,33697
88
+ execsql/script/executor.py,sha256=Y0con6Mz4n240krEVeVSqmeCLVvIXyF4aLlNBMyZQyc,35790
89
+ execsql/script/parser.py,sha256=z19gERLqyvEoDx4hDUws3Z8EKqTM7huNOitTOSmg8_0,35300
90
90
  execsql/script/variables.py,sha256=t0BwrRuA8m1LYHGLkDPNbqW6QmudXroOFYsO0fwK2N0,16302
91
91
  execsql/utils/__init__.py,sha256=0uR6JwVJQRX3vceByNBduCAf5dd5assKjeqJUWvpZoA,278
92
92
  execsql/utils/auth.py,sha256=dnLie8jFxN_l7ZrrRufVuxGw92iG62DIVatIjlEb4pM,8717
@@ -100,24 +100,24 @@ execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
100
100
  execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
101
101
  execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
102
102
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
103
- execsql2-2.18.1.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
104
- execsql2-2.18.1.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
105
- execsql2-2.18.1.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
106
- execsql2-2.18.1.data/data/execsql2_extras/execsql.conf,sha256=1a2g2Vga7s128wcu3ftIFRkHlKKtuvkuOHSD1XuNT7o,9404
107
- execsql2-2.18.1.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
108
- execsql2-2.18.1.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
109
- execsql2-2.18.1.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
110
- execsql2-2.18.1.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
111
- execsql2-2.18.1.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
112
- execsql2-2.18.1.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
113
- execsql2-2.18.1.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
114
- execsql2-2.18.1.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
115
- execsql2-2.18.1.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
116
- execsql2-2.18.1.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
117
- execsql2-2.18.1.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
118
- execsql2-2.18.1.dist-info/METADATA,sha256=czc8snw8MBbrf19X5tlYXFe5cCcpSFQmE6_pi0ezAYk,21911
119
- execsql2-2.18.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
120
- execsql2-2.18.1.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
121
- execsql2-2.18.1.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
122
- execsql2-2.18.1.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
123
- execsql2-2.18.1.dist-info/RECORD,,
103
+ execsql2-2.19.0.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
104
+ execsql2-2.19.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
105
+ execsql2-2.19.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=2e8KzzVWhho8KxYVHETSVmZdhW7wodioDsqBLSL6m4s,7487
106
+ execsql2-2.19.0.data/data/execsql2_extras/execsql.conf,sha256=Sq1Huwb_Uf_lI7zW7m3h11fqU6zjGpGnCU4OVJ_tCe0,10696
107
+ execsql2-2.19.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
108
+ execsql2-2.19.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
109
+ execsql2-2.19.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
110
+ execsql2-2.19.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
111
+ execsql2-2.19.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
112
+ execsql2-2.19.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
113
+ execsql2-2.19.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
114
+ execsql2-2.19.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
115
+ execsql2-2.19.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
116
+ execsql2-2.19.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
117
+ execsql2-2.19.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
118
+ execsql2-2.19.0.dist-info/METADATA,sha256=hafmsG77iDxgwzrG7s4oMg5o97R-fMx_BH513avRy2w,22340
119
+ execsql2-2.19.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
120
+ execsql2-2.19.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
121
+ execsql2-2.19.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
122
+ execsql2-2.19.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
123
+ execsql2-2.19.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any