execsql2 2.16.18__py3-none-any.whl → 2.17.2__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 (74) hide show
  1. execsql/__init__.py +6 -2
  2. execsql/api.py +25 -6
  3. execsql/cli/__init__.py +5 -3
  4. execsql/cli/lint.py +30 -34
  5. execsql/cli/run.py +10 -0
  6. execsql/config.py +145 -92
  7. execsql/db/access.py +54 -40
  8. execsql/db/base.py +33 -6
  9. execsql/db/firebird.py +3 -1
  10. execsql/db/mysql.py +4 -3
  11. execsql/db/oracle.py +36 -14
  12. execsql/db/postgres.py +8 -6
  13. execsql/db/sqlite.py +5 -2
  14. execsql/db/sqlserver.py +8 -6
  15. execsql/debug/repl.py +59 -21
  16. execsql/exceptions.py +19 -4
  17. execsql/exporters/base.py +3 -2
  18. execsql/exporters/delimited.py +2 -3
  19. execsql/exporters/feather.py +3 -3
  20. execsql/exporters/ods.py +1 -1
  21. execsql/exporters/xls.py +12 -4
  22. execsql/exporters/xlsx.py +1 -1
  23. execsql/gui/desktop.py +129 -15
  24. execsql/importers/__init__.py +1 -1
  25. execsql/importers/ods.py +1 -1
  26. execsql/importers/xls.py +1 -1
  27. execsql/metacommands/__init__.py +34 -5
  28. execsql/metacommands/conditions.py +26 -14
  29. execsql/metacommands/connect.py +21 -14
  30. execsql/metacommands/control.py +55 -68
  31. execsql/metacommands/data.py +25 -9
  32. execsql/metacommands/debug.py +132 -77
  33. execsql/metacommands/io_export.py +14 -2
  34. execsql/metacommands/io_import.py +11 -2
  35. execsql/metacommands/io_write.py +113 -11
  36. execsql/metacommands/prompt.py +46 -32
  37. execsql/metacommands/script_ext.py +63 -34
  38. execsql/metacommands/system.py +4 -3
  39. execsql/metacommands/upsert.py +0 -29
  40. execsql/script/__init__.py +28 -37
  41. execsql/script/ast.py +7 -7
  42. execsql/script/control.py +4 -101
  43. execsql/script/engine.py +37 -251
  44. execsql/script/executor.py +193 -230
  45. execsql/script/parser.py +1 -3
  46. execsql/script/variables.py +8 -3
  47. execsql/state.py +125 -37
  48. execsql/utils/errors.py +0 -2
  49. execsql/utils/fileio.py +47 -3
  50. execsql/utils/mail.py +3 -2
  51. execsql/utils/strings.py +5 -5
  52. {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/METADATA +42 -36
  53. execsql2-2.17.2.dist-info/RECORD +124 -0
  54. execsql2-2.17.2.dist-info/licenses/NOTICE +11 -0
  55. execsql2-2.16.18.dist-info/RECORD +0 -124
  56. execsql2-2.16.18.dist-info/licenses/NOTICE +0 -10
  57. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/README.md +0 -0
  58. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  59. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  60. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/execsql.conf +0 -0
  61. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
  62. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_compare.sql +0 -0
  63. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
  64. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
  65. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
  66. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  67. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  68. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/script_template.sql +0 -0
  69. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
  70. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  71. {execsql2-2.16.18.data → execsql2-2.17.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  72. {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/WHEEL +0 -0
  73. {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/entry_points.txt +0 -0
  74. {execsql2-2.16.18.dist-info → execsql2-2.17.2.dist-info}/licenses/LICENSE.txt +0 -0
execsql/script/engine.py CHANGED
@@ -2,28 +2,25 @@ from __future__ import annotations
2
2
 
3
3
  """Script execution engine for execsql.
4
4
 
5
- Classes and functions that load, parse, and drive execution of execsql
6
- ``.sql`` script files.
5
+ Holds the metacommand dispatch primitives, the statement data types
6
+ consumed by the AST executor, and the substitution-variable helpers
7
+ shared by the parser and executor. Script parsing and tree-walking
8
+ live in :mod:`execsql.script.parser` and
9
+ :mod:`execsql.script.executor` respectively.
7
10
 
8
11
  Classes:
9
12
  - :class:`MetaCommand` — one entry in the metacommand dispatch table.
10
- - :class:`MetaCommandList` — ordered list of :class:`MetaCommand` entries.
13
+ - :class:`MetaCommandList` — ordered list of :class:`MetaCommand` entries with a keyword index.
11
14
  - :class:`SqlStmt` — wraps a single SQL string for execution.
12
15
  - :class:`MetacommandStmt` — wraps a metacommand line for dispatch.
13
16
  - :class:`ScriptCmd` — pairs a statement with its source-file location.
14
- - :class:`CommandList` — ordered list of :class:`ScriptCmd` objects.
15
- - :class:`CommandListWhileLoop` — loop variant that repeats while a condition is true.
16
- - :class:`CommandListUntilLoop` — loop variant that repeats until a condition is true.
17
- - :class:`ScriptFile` — reads and tokenises a ``.sql`` file.
18
17
  - :class:`ScriptExecSpec` — specification for deferred script execution.
19
18
 
20
19
  Functions:
21
- - :func:`set_system_vars` — populates built-in ``$VARNAME`` system variables.
22
- - :func:`substitute_vars` — performs ``!!$VAR!!`` and ``!{$var}!`` expansion.
23
- - :func:`runscripts` — central execution loop.
24
- - :func:`current_script_line` — returns the source location of the currently executing command.
25
- - :func:`read_sqlfile` — parses a SQL script file into a new :class:`CommandList`.
26
- - :func:`read_sqlstring` — parses an inline script string into a new :class:`CommandList`.
20
+ - :func:`set_system_vars` — populates built-in ``$VARNAME`` system variables (calls the static + dynamic helpers).
21
+ - :func:`set_static_system_vars` / :func:`set_dynamic_system_vars` refresh the half-static / per-statement system variables independently.
22
+ - :func:`substitute_vars` — performs ``!!$VAR!!`` / ``!'!var!'!`` / ``!"!var!"!`` / ``!{$var}!`` expansion.
23
+ - :func:`current_script_line` — returns the ``(file, line_no)`` of the currently executing command.
27
24
  """
28
25
 
29
26
  import datetime
@@ -35,7 +32,7 @@ from typing import Any
35
32
 
36
33
  import execsql.state as _state
37
34
  from execsql.exceptions import ErrInfo
38
- from execsql.script.variables import LocalSubVarSet, SubVarSet
35
+ from execsql.script.variables import SubVarSet
39
36
  from execsql.utils.errors import exception_desc
40
37
 
41
38
  __all__ = [
@@ -44,7 +41,6 @@ __all__ = [
44
41
  "SqlStmt",
45
42
  "MetacommandStmt",
46
43
  "ScriptCmd",
47
- "CommandList",
48
44
  "ScriptExecSpec",
49
45
  "set_system_vars",
50
46
  "substitute_vars",
@@ -237,10 +233,9 @@ class MetaCommandList:
237
233
  run, ``(False, None)`` if no command matched.
238
234
  """
239
235
  for cmd in self._candidates(cmd_str):
240
- if _state.if_stack.all_true() or cmd.run_when_false:
241
- success, value = cmd.run(cmd_str)
242
- if success:
243
- return True, value
236
+ success, value = cmd.run(cmd_str)
237
+ if success:
238
+ return True, value
244
239
  return False, None
245
240
 
246
241
  def get_match(self, cmd: str) -> tuple | None:
@@ -261,109 +256,38 @@ class MetaCommandList:
261
256
 
262
257
 
263
258
  class SqlStmt:
264
- """A single SQL statement ready to be executed against the active database."""
259
+ """A single SQL statement ready to be executed against the active database.
260
+
261
+ Data class only — the legacy ``.run()`` method was removed when the AST
262
+ executor became the sole engine. SQL execution now goes through
263
+ :func:`execsql.script.executor._exec_sql`.
264
+ """
265
265
 
266
- # A SQL statement to be passed to a database to execute.
267
266
  def __init__(self, sql_statement: str) -> None:
268
267
  self.statement = re.sub(r"\s*;(\s*;\s*)+$", ";", sql_statement)
269
268
 
270
269
  def __repr__(self) -> str:
271
270
  return f"SqlStmt({self.statement})"
272
271
 
273
- def run(self, localvars: SubVarSet | None = None, commit: bool = True) -> None:
274
- """Execute the statement on the current database, committing unless in a batch."""
275
- # Run the SQL statement on the current database.
276
- if _state.if_stack.all_true():
277
- e = None
278
- _state.status.sql_error = False
279
- cmd = substitute_vars(self.statement, localvars)
280
- if _state.varlike.search(cmd):
281
- _state.output.write(
282
- f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
283
- )
284
- try:
285
- db = _state.dbs.current()
286
- if _state.conf.log_sql and _state.exec_log:
287
- lno = getattr(_state, "last_command", None)
288
- lno = lno.line_no if lno and hasattr(lno, "line_no") else None
289
- _state.exec_log.log_sql_query(cmd, db.name(), lno)
290
- db.execute(cmd)
291
- if commit:
292
- db.commit()
293
- except ErrInfo as errinfo:
294
- e = errinfo
295
- except SystemExit:
296
- raise
297
- except Exception:
298
- e = ErrInfo(type="exception", exception_msg=exception_desc())
299
- if e:
300
- from execsql.utils.errors import stamp_errinfo
301
-
302
- stamp_errinfo(e)
303
- _state.subvars.add_substitution("$LAST_ERROR", cmd)
304
- _state.subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
305
- _state.status.sql_error = True
306
- if _state.exec_log is not None:
307
- _state.exec_log.log_status_info(f"SQL error: {e.errmsg()}")
308
- if _state.status.halt_on_err:
309
- from execsql.utils.errors import exit_now
310
-
311
- exit_now(1, e)
312
- return
313
- _state.subvars.add_substitution("$LAST_SQL", cmd)
314
-
315
272
  def commandline(self) -> str:
316
273
  """Return the raw SQL statement text."""
317
274
  return self.statement
318
275
 
319
276
 
320
277
  class MetacommandStmt:
321
- """A single execsql metacommand line ready to be dispatched."""
278
+ """A single execsql metacommand line.
279
+
280
+ Data class only — the legacy ``.run()`` method was removed when the AST
281
+ executor became the sole engine. Metacommand dispatch now goes through
282
+ :func:`execsql.script.executor._exec_metacommand`.
283
+ """
322
284
 
323
- # A metacommand to be handled by execsql.
324
285
  def __init__(self, metacommand_statement: str) -> None:
325
286
  self.statement = metacommand_statement
326
287
 
327
288
  def __repr__(self) -> str:
328
289
  return f"MetacommandStmt({self.statement})"
329
290
 
330
- def run(self, localvars: SubVarSet | None = None, commit: bool = False) -> Any:
331
- """Expand substitution variables then dispatch through the metacommand table."""
332
- # Tries all metacommands in the dispatch table until one runs.
333
- errmsg = "Unknown metacommand"
334
- cmd = substitute_vars(self.statement, localvars)
335
- if _state.if_stack.all_true() and _state.varlike.search(cmd):
336
- _state.output.write(f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n")
337
- e = None
338
- try:
339
- applies, result = _state.metacommandlist.eval(cmd)
340
- if applies:
341
- return result
342
- except ErrInfo as errinfo:
343
- e = errinfo
344
- except SystemExit:
345
- raise
346
- except Exception:
347
- e = ErrInfo(type="exception", exception_msg=exception_desc())
348
- if e:
349
- from execsql.utils.errors import stamp_errinfo
350
-
351
- stamp_errinfo(e)
352
- _state.status.metacommand_error = True
353
- _state.subvars.add_substitution("$LAST_ERROR", cmd)
354
- _state.subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
355
- if _state.exec_log is not None:
356
- _state.exec_log.log_status_info(f"Metacommand error: {e.errmsg()}")
357
- if _state.status.halt_on_metacommand_err:
358
- # Re-raise the original ErrInfo so its message is preserved, not
359
- # replaced with the generic "Unknown metacommand" text.
360
- raise e
361
- if _state.if_stack.all_true():
362
- # but nothing applies, because we got here.
363
- _state.status.metacommand_error = True
364
- raise ErrInfo(type="cmd", command_text=cmd, other_msg=errmsg)
365
- return None
366
-
367
291
  def commandline(self) -> str:
368
292
  """Return the metacommand line in its canonical ``-- !x! ...`` form."""
369
293
  return "-- !x! " + self.statement
@@ -409,145 +333,6 @@ class ScriptCmd:
409
333
  return self.command.statement if self.command_type == "sql" else "-- !x! " + self.command.statement
410
334
 
411
335
 
412
- # ---------------------------------------------------------------------------
413
- # CommandList / CommandListWhileLoop / CommandListUntilLoop
414
- # ---------------------------------------------------------------------------
415
-
416
-
417
- class CommandList:
418
- """Ordered sequence of :class:`ScriptCmd` objects with a forward-only execution cursor.
419
-
420
- Push onto ``_state.commandliststack`` and call :meth:`run_next` in a loop
421
- (or let :func:`runscripts` drive it) to execute each command in turn.
422
- """
423
-
424
- # A list of ScriptCmd objects including execution state.
425
- def __init__(
426
- self,
427
- cmdlist: list[ScriptCmd],
428
- listname: str,
429
- paramnames: list[str] | None = None,
430
- ) -> None:
431
- if cmdlist is None:
432
- raise ErrInfo("error", other_msg="Initiating a command list without any commands.")
433
- self.listname = listname
434
- self.cmdlist = cmdlist
435
- self.cmdptr = 0
436
- self.paramnames = paramnames
437
- self.paramvals: SubVarSet | None = None
438
- self.localvars = LocalSubVarSet()
439
- self.init_if_level: int | None = None
440
-
441
- def add(self, script_command: ScriptCmd) -> None:
442
- """Append *script_command* to the end of this command list."""
443
- self.cmdlist.append(script_command)
444
-
445
- def set_paramvals(self, paramvals: SubVarSet) -> None:
446
- self.paramvals = paramvals
447
- if self.paramnames is not None:
448
- passed_paramnames = [p[0].lstrip("#") for p in paramvals.substitutions]
449
- if not all(p in passed_paramnames for p in self.paramnames):
450
- raise ErrInfo(
451
- "error",
452
- other_msg=f"Formal and actual parameter name mismatch in call to {self.listname}.",
453
- )
454
-
455
- def current_command(self) -> ScriptCmd | None:
456
- """Return the :class:`ScriptCmd` at the current cursor position, or ``None`` if exhausted."""
457
- if self.cmdptr > len(self.cmdlist) - 1:
458
- return None
459
- return self.cmdlist[self.cmdptr]
460
-
461
- def check_iflevels(self) -> None:
462
- """Warn if the IF-stack depth changed during execution of this command list."""
463
- if_excess = len(_state.if_stack.if_levels) - self.init_if_level
464
- if if_excess > 0:
465
- sources = _state.if_stack.script_lines(if_excess)
466
- src_msg = ", ".join([f"{src[0]} line {src[1]}" for src in sources])
467
- from execsql.utils.errors import write_warning
468
-
469
- write_warning(f"IF level mismatch at beginning and end of script; origin at or after: {src_msg}.")
470
-
471
- def run_and_increment(self) -> None:
472
- cmditem = self.cmdlist[self.cmdptr]
473
- if _state.compiling_loop:
474
- # Don't run this command, but save it or complete the loop.
475
- if cmditem.command_type == "cmd" and _state.loop_rx.match(cmditem.command.statement):
476
- _state.loop_nest_level += 1
477
- # Substitute any deferred substitution variables with regular substitution var flags.
478
- m = _state.defer_rx.findall(cmditem.command.statement)
479
- if m is not None:
480
- for dv in m:
481
- rep = "!!" + dv[1] + "!!"
482
- cmditem.command.statement = cmditem.command.statement.replace(dv[0], rep)
483
- _state.loopcommandstack[-1].add(cmditem)
484
- elif cmditem.command_type == "cmd" and _state.endloop_rx.match(cmditem.command.statement):
485
- if _state.loop_nest_level == 0:
486
- _state.endloop()
487
- else:
488
- _state.loop_nest_level -= 1
489
- _state.loopcommandstack[-1].add(cmditem)
490
- else:
491
- _state.loopcommandstack[-1].add(cmditem)
492
- else:
493
- _state.last_command = cmditem
494
- if cmditem.command_type == "sql" and _state.status.batch.in_batch():
495
- _state.status.batch.using_db(_state.dbs.current())
496
- _state.subvars.add_substitution("$CURRENT_TIME", datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))
497
- utcnow = datetime.datetime.now(tz=datetime.timezone.utc)
498
- _state.subvars.add_substitution("$CURRENT_TIME_UTC", utcnow.strftime("%Y-%m-%d %H:%M"))
499
- _state.subvars.add_substitution("$CURRENT_SCRIPT", cmditem.source)
500
- _state.subvars.add_substitution(
501
- "$CURRENT_SCRIPT_PATH",
502
- cmditem.source_dir,
503
- )
504
- _state.subvars.add_substitution("$CURRENT_SCRIPT_NAME", cmditem.source_name)
505
- _state.subvars.add_substitution("$CURRENT_SCRIPT_LINE", str(cmditem.line_no))
506
- _state.subvars.add_substitution("$SCRIPT_LINE", str(cmditem.line_no))
507
- if _state.step_mode:
508
- _state.step_mode = False
509
- from execsql.debug.repl import _debug_repl
510
-
511
- _debug_repl(step=True)
512
- _profiling = _state.profile_data is not None
513
- if _profiling:
514
- import time as _time
515
-
516
- _t0 = _time.perf_counter()
517
- cmditem.command.run(self.localvars.merge(self.paramvals), not _state.status.batch.in_batch())
518
- if _profiling:
519
- _elapsed = _time.perf_counter() - _t0
520
- _state.profile_data.append(
521
- (
522
- cmditem.source,
523
- cmditem.line_no,
524
- cmditem.command_type,
525
- _elapsed,
526
- cmditem.command.commandline()[:100],
527
- ),
528
- )
529
- self.cmdptr += 1
530
-
531
- def run_next(self) -> None:
532
- """Execute the command at the current cursor and advance; raise StopIteration when done."""
533
- if self.cmdptr == 0:
534
- self.init_if_level = len(_state.if_stack.if_levels)
535
- if self.cmdptr > len(self.cmdlist) - 1:
536
- self.check_iflevels()
537
- raise StopIteration
538
- self.run_and_increment()
539
-
540
- def __iter__(self) -> Any:
541
- return self
542
-
543
- def __next__(self) -> ScriptCmd:
544
- if self.cmdptr > len(self.cmdlist) - 1:
545
- raise StopIteration
546
- scriptcmd = self.cmdlist[self.cmdptr]
547
- self.cmdptr += 1
548
- return scriptcmd
549
-
550
-
551
336
  # ---------------------------------------------------------------------------
552
337
  # ScriptExecSpec
553
338
  # ---------------------------------------------------------------------------
@@ -564,7 +349,7 @@ class ScriptExecSpec:
564
349
 
565
350
  def __init__(self, **kwargs: Any) -> None:
566
351
  self.script_id = kwargs["script_id"].lower()
567
- if self.script_id not in _state.savedscripts:
352
+ if self.script_id not in _state.ast_scripts:
568
353
  raise ErrInfo("cmd", other_msg=f"There is no SCRIPT named {self.script_id}.")
569
354
  self.arg_exp = kwargs["argexp"]
570
355
  self.looptype = kwargs["looptype"].upper() if "looptype" in kwargs and kwargs["looptype"] is not None else None
@@ -640,7 +425,7 @@ def set_dynamic_system_vars(ctx: Any = None) -> None:
640
425
  _s.subvars.add_substitution("$CONSOLE_WAIT_WHEN_DONE_STATE", "ON" if _s.conf.gui_wait_on_exit else "OFF")
641
426
  db = _s.dbs.current()
642
427
  _s.subvars.add_substitution("$AUTOCOMMIT_STATE", "ON" if db.autocommit else "OFF")
643
- # $CURRENT_TIME is set per-statement in run_and_increment() for accuracy.
428
+ # $CURRENT_TIME is set per-statement in executor._set_command_vars() for accuracy.
644
429
  _s.subvars.add_substitution("$TIMER", str(datetime.timedelta(seconds=_s.timer.elapsed())))
645
430
  _s.subvars.clear_lazy_cache()
646
431
 
@@ -700,12 +485,13 @@ def substitute_vars(command_str: str, localvars: SubVarSet | None = None, ctx: A
700
485
 
701
486
 
702
487
  def current_script_line() -> tuple:
703
- """Return ``(source_name, line_number)`` for the command currently executing."""
704
- if len(_state.commandliststack) > 0:
705
- current_cmds = _state.commandliststack[-1]
706
- if current_cmds.current_command() is not None:
707
- return current_cmds.current_command().current_script_line()
708
- else:
709
- return (f"script '{current_cmds.listname}'", len(current_cmds.cmdlist))
710
- else:
488
+ """Return ``(source_name, line_number)`` for the command currently executing.
489
+
490
+ Reads from ``_state.last_command``, which the AST executor updates on
491
+ every statement via the ``_FakeScriptCmd`` shim. Returns ``("", 0)``
492
+ when nothing has executed yet (e.g. during early initialization errors).
493
+ """
494
+ last = getattr(_state, "last_command", None)
495
+ if last is None:
711
496
  return ("", 0)
497
+ return (getattr(last, "source", ""), getattr(last, "line_no", 0))