execsql2 2.34.0__py3-none-any.whl → 2.37.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 (43) hide show
  1. execsql/api.py +17 -8
  2. execsql/cli/lint.py +54 -6
  3. execsql/cli/run.py +5 -10
  4. execsql/config.py +2 -0
  5. execsql/data/execsql.conf.template +17 -7
  6. execsql/db/base.py +12 -2
  7. execsql/db/duckdb.py +8 -1
  8. execsql/debug/repl.py +2 -2
  9. execsql/metacommands/connect.py +10 -9
  10. execsql/metacommands/data.py +7 -1
  11. execsql/metacommands/dispatch.py +7 -0
  12. execsql/metacommands/io_fileops.py +6 -3
  13. execsql/metacommands/io_write.py +3 -1
  14. execsql/metacommands/reference.py +1 -0
  15. execsql/metacommands/script_ext.py +13 -10
  16. execsql/metacommands/system.py +13 -6
  17. execsql/models.py +30 -4
  18. execsql/script/ast.py +21 -0
  19. execsql/script/control.py +63 -3
  20. execsql/script/executor.py +379 -229
  21. execsql/script/parser.py +102 -52
  22. execsql/state.py +23 -0
  23. execsql/utils/errors.py +24 -6
  24. execsql/utils/sigterm.py +154 -0
  25. {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/METADATA +2 -2
  26. {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/RECORD +43 -42
  27. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/README.md +0 -0
  28. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  29. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  30. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  31. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  32. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  33. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  34. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  35. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  36. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/script_template.sql +0 -0
  37. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  38. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  39. {execsql2-2.34.0.data → execsql2-2.37.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  40. {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/WHEEL +0 -0
  41. {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/entry_points.txt +0 -0
  42. {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/licenses/LICENSE.txt +0 -0
  43. {execsql2-2.34.0.dist-info → execsql2-2.37.0.dist-info}/licenses/NOTICE +0 -0
execsql/script/ast.py CHANGED
@@ -212,12 +212,15 @@ class ElseIfClause:
212
212
  condition_modifiers: ANDIF/ORIF modifiers that compound the ELSEIF
213
213
  condition, evaluated left-to-right at runtime.
214
214
  body: Nodes executed when this condition is true.
215
+ command: The metacommand as written, without ``-- !x!``, for error
216
+ reports.
215
217
  """
216
218
 
217
219
  condition: str
218
220
  span: SourceSpan
219
221
  condition_modifiers: list[ConditionModifier] = field(default_factory=list)
220
222
  body: list[Node] = field(default_factory=list)
223
+ command: str = ""
221
224
 
222
225
 
223
226
  @dataclass
@@ -232,6 +235,8 @@ class IfBlock(Node):
232
235
  elseif_clauses: Zero or more ELSEIF branches, evaluated in order.
233
236
  else_body: Nodes executed when the IF condition (and all ELSEIFs)
234
237
  are false. Empty list means no ELSE branch.
238
+ command: The metacommand as written, without ``-- !x!``, for error
239
+ reports.
235
240
  """
236
241
 
237
242
  condition: str
@@ -240,6 +245,7 @@ class IfBlock(Node):
240
245
  elseif_clauses: list[ElseIfClause] = field(default_factory=list)
241
246
  else_body: list[Node] = field(default_factory=list)
242
247
  else_span: SourceSpan | None = None
248
+ command: str = ""
243
249
 
244
250
  def children(self) -> Iterator[Node]:
245
251
  yield from self.body
@@ -261,11 +267,14 @@ class LoopBlock(Node):
261
267
  loop_type: Either ``"WHILE"`` or ``"UNTIL"``.
262
268
  condition: The condition expression text.
263
269
  body: Nodes executed on each iteration.
270
+ command: The metacommand as written, without ``-- !x!``, for error
271
+ reports.
264
272
  """
265
273
 
266
274
  loop_type: str # "WHILE" or "UNTIL"
267
275
  condition: str
268
276
  body: list[Node] = field(default_factory=list)
277
+ command: str = ""
269
278
 
270
279
  def children(self) -> Iterator[Node]:
271
280
  yield from self.body
@@ -283,9 +292,12 @@ class BatchBlock(Node):
283
292
 
284
293
  Attributes:
285
294
  body: Nodes within the batch.
295
+ command: The metacommand as written, without ``-- !x!``, for error
296
+ reports.
286
297
  """
287
298
 
288
299
  body: list[Node] = field(default_factory=list)
300
+ command: str = ""
289
301
 
290
302
  def children(self) -> Iterator[Node]:
291
303
  yield from self.body
@@ -325,12 +337,15 @@ class ScriptBlock(Node):
325
337
  doc: Optional docstring extracted from comments immediately following
326
338
  the BEGIN SCRIPT line.
327
339
  body: Nodes within the script block.
340
+ command: The metacommand as written, without ``-- !x!``, for error
341
+ reports.
328
342
  """
329
343
 
330
344
  name: str
331
345
  param_defs: list[ParamDef] | None = None
332
346
  doc: str | None = None
333
347
  body: list[Node] = field(default_factory=list)
348
+ command: str = ""
334
349
 
335
350
  @property
336
351
  def param_names(self) -> list[str] | None:
@@ -358,9 +373,12 @@ class SqlBlock(Node):
358
373
  Attributes:
359
374
  body: Nodes within the SQL block (typically a single
360
375
  :class:`SqlStatement`).
376
+ command: The metacommand as written, without ``-- !x!``, for error
377
+ reports.
361
378
  """
362
379
 
363
380
  body: list[Node] = field(default_factory=list)
381
+ command: str = ""
364
382
 
365
383
  def children(self) -> Iterator[Node]:
366
384
  yield from self.body
@@ -384,6 +402,8 @@ class IncludeDirective(Node):
384
402
  arguments: Optional argument expression for EXECUTE SCRIPT.
385
403
  loop_type: Optional ``"WHILE"`` or ``"UNTIL"`` for looped execution.
386
404
  loop_condition: The loop condition expression, if *loop_type* is set.
405
+ command: The metacommand as written, without ``-- !x!``, for error
406
+ reports.
387
407
  """
388
408
 
389
409
  target: str
@@ -392,6 +412,7 @@ class IncludeDirective(Node):
392
412
  arguments: str | None = None
393
413
  loop_type: str | None = None
394
414
  loop_condition: str | None = None
415
+ command: str = ""
395
416
 
396
417
  def __repr__(self) -> str:
397
418
  kind = "EXECUTE SCRIPT" if self.is_execute_script else "INCLUDE"
execsql/script/control.py CHANGED
@@ -30,6 +30,8 @@ class BatchLevels:
30
30
  class Batch:
31
31
  def __init__(self) -> None:
32
32
  self.dbs_used: list[Any] = []
33
+ # Why this batch can no longer commit (an error inside it), or None.
34
+ self.failed: str | None = None
33
35
 
34
36
  def __init__(self) -> None:
35
37
  self.batchlevels: list[BatchLevels.Batch] = []
@@ -38,6 +40,25 @@ class BatchLevels:
38
40
  """Return True if execution is currently inside at least one BATCH block."""
39
41
  return len(self.batchlevels) > 0
40
42
 
43
+ def fail_enclosing(self, reason: str) -> None:
44
+ """Mark every open batch level as failed, so its END BATCH rolls back instead of committing."""
45
+ for b in self.batchlevels:
46
+ if b.failed is None:
47
+ b.failed = reason
48
+
49
+ def failure(self) -> str | None:
50
+ """Why the innermost batch can no longer commit, or None.
51
+
52
+ A batch inside a failed one cannot commit either: its commit would
53
+ commit the failed batch's later statements on a shared connection.
54
+ Nothing clears the mark, ROLLBACK BATCH included: once anything in a
55
+ batch has failed, the whole stack of batches rolls back.
56
+ """
57
+ for b in reversed(self.batchlevels):
58
+ if b.failed is not None:
59
+ return b.failed
60
+ return None
61
+
41
62
  def new_batch(self) -> None:
42
63
  """Push a new empty batch level onto the stack."""
43
64
  self.batchlevels.append(self.Batch())
@@ -61,7 +82,46 @@ class BatchLevels:
61
82
  db.rollback()
62
83
 
63
84
  def end_batch(self) -> None:
64
- """Commit all databases in the innermost batch level and pop the stack."""
85
+ """Commit all databases in the innermost batch level and pop the stack.
86
+
87
+ If a commit fails, :meth:`Database.commit` rolls that database back,
88
+ and the ones not yet committed are rolled back too (except one with
89
+ autocommit off, whose transaction is the script's) before the error
90
+ is raised: a commit that already succeeded
91
+ cannot be undone, but nothing else of the batch is left pending for
92
+ a later statement to commit.
93
+ """
94
+ b = self.batchlevels.pop()
95
+ for i, db in enumerate(b.dbs_used):
96
+ try:
97
+ db.commit()
98
+ except BaseException:
99
+ _roll_back(b.dbs_used[i + 1 :])
100
+ raise
101
+
102
+ def abandon_batch(self) -> None:
103
+ """Pop the innermost batch level without committing it.
104
+
105
+ Inside another batch, its databases join the enclosing level, which
106
+ commits or rolls them back. Otherwise they are rolled back, except
107
+ one with autocommit off: :meth:`end_batch` would not have committed
108
+ it either, so its transaction stays the script's to end.
109
+ """
65
110
  b = self.batchlevels.pop()
66
- for db in b.dbs_used:
67
- db.commit()
111
+ if self.batchlevels:
112
+ for db in b.dbs_used:
113
+ self.using_db(db)
114
+ return
115
+ _roll_back(b.dbs_used)
116
+
117
+ def abandon_all(self) -> None:
118
+ """Abandon every open batch level, innermost first."""
119
+ while self.batchlevels:
120
+ self.abandon_batch()
121
+
122
+
123
+ def _roll_back(dbs: list[Any]) -> None:
124
+ """Roll back each of *dbs* except one with autocommit off, whose transaction is the script's."""
125
+ for db in dbs:
126
+ if db.autocommit:
127
+ db.rollback()