execsql2 2.4.5__py3-none-any.whl → 2.6.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 (62) hide show
  1. execsql/cli/__init__.py +14 -0
  2. execsql/cli/dsn.py +2 -0
  3. execsql/cli/help.py +2 -0
  4. execsql/cli/run.py +4 -2
  5. execsql/constants.py +11 -0
  6. execsql/db/access.py +20 -0
  7. execsql/db/base.py +4 -0
  8. execsql/db/dsn.py +11 -8
  9. execsql/db/duckdb.py +12 -8
  10. execsql/db/firebird.py +17 -8
  11. execsql/db/mysql.py +13 -8
  12. execsql/db/oracle.py +22 -8
  13. execsql/db/postgres.py +21 -9
  14. execsql/db/sqlite.py +18 -8
  15. execsql/db/sqlserver.py +14 -8
  16. execsql/exporters/__init__.py +6 -1
  17. execsql/exporters/base.py +2 -0
  18. execsql/exporters/delimited.py +10 -0
  19. execsql/exporters/protocol.py +128 -0
  20. execsql/exporters/xls.py +8 -0
  21. execsql/format.py +3 -1
  22. execsql/gui/__init__.py +2 -0
  23. execsql/gui/base.py +2 -0
  24. execsql/gui/console.py +2 -0
  25. execsql/gui/desktop.py +1 -0
  26. execsql/gui/tui.py +134 -0
  27. execsql/importers/base.py +1 -0
  28. execsql/importers/csv.py +2 -0
  29. execsql/importers/feather.py +2 -0
  30. execsql/importers/ods.py +1 -0
  31. execsql/importers/xls.py +1 -0
  32. execsql/metacommands/__init__.py +386 -180
  33. execsql/metacommands/dispatch.py +2 -0
  34. execsql/metacommands/io.py +41 -0
  35. execsql/models.py +17 -0
  36. execsql/parser.py +41 -0
  37. execsql/script/control.py +2 -0
  38. execsql/script/engine.py +19 -0
  39. execsql/script/variables.py +9 -5
  40. execsql/state.py +312 -199
  41. execsql/types.py +46 -0
  42. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/METADATA +2 -2
  43. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/RECORD +62 -61
  44. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/README.md +0 -0
  45. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  46. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  47. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/execsql.conf +0 -0
  48. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  49. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  50. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  51. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  52. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  53. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  54. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  55. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/script_template.sql +0 -0
  56. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  57. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  58. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  59. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/WHEEL +0 -0
  60. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/entry_points.txt +0 -0
  61. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/licenses/LICENSE.txt +0 -0
  62. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/licenses/NOTICE +0 -0
execsql/models.py CHANGED
@@ -49,13 +49,18 @@ __all__ = [
49
49
 
50
50
 
51
51
  class Column:
52
+ """Compile data-type match statistics for a single column of imported data."""
53
+
52
54
  # Column objects are used to compile information about the data types that a set of data
53
55
  # values may match. A Column object is intended to be used to identify the data type of a column
54
56
  # when scanning a data stream (such as a CSV file) to create a new data table.
55
57
 
56
58
  class Accum:
59
+ """Accumulate match counts and length statistics for a single data type."""
60
+
57
61
  # Accumulates the count of matches for each data type, plus the maximum length if appropriate.
58
62
  def __init__(self, data_type_obj: DataType) -> None:
63
+ """Initialise the accumulator for the given data type."""
59
64
  self.dt = data_type_obj
60
65
  self.failed = False
61
66
  self.count = 0
@@ -72,6 +77,7 @@ class Column:
72
77
  )
73
78
 
74
79
  def check(self, datavalue: Any) -> None:
80
+ """Test whether a non-null value matches this data type and update statistics."""
75
81
  # datavalue must be non-null
76
82
  if not self.failed:
77
83
  is_match = self.dt.matches(datavalue)
@@ -105,6 +111,7 @@ class Column:
105
111
  self.failed = True
106
112
 
107
113
  def __init__(self, colname: str) -> None:
114
+ """Create a column characteriser for the named column."""
108
115
  from execsql.exceptions import ErrInfo
109
116
  import execsql.state as _state
110
117
 
@@ -150,6 +157,7 @@ class Column:
150
157
  return f"Column({self.name!r})"
151
158
 
152
159
  def eval_types(self, column_value: Any) -> None:
160
+ """Evaluate which data types the value matches and update counters."""
153
161
  # Evaluate which data type(s) the value matches, and increment the appropriate counter(s).
154
162
  import execsql.state as _state
155
163
 
@@ -170,6 +178,7 @@ class Column:
170
178
  dt.check(column_value)
171
179
 
172
180
  def column_type(self) -> tuple:
181
+ """Return the inferred type of this column as a 6-tuple."""
173
182
  # Return the type of this column as a tuple of:
174
183
  # column name, data type class, max length or None, bool for null values,
175
184
  # precision or None, scale or None.
@@ -212,7 +221,10 @@ class Column:
212
221
 
213
222
 
214
223
  class DataTable:
224
+ """Scan a row source and infer column types for CREATE TABLE generation."""
225
+
215
226
  def __init__(self, column_names: list[str], rowsource: Any) -> None:
227
+ """Scan all rows from the source and infer a column type for each column."""
216
228
  import execsql.state as _state
217
229
 
218
230
  self.inputrows = 0 # Total number of rows in the row source.
@@ -264,6 +276,7 @@ class DataTable:
264
276
  return f"DataTable({[col.name for col in self.cols]!r}, rowsource)"
265
277
 
266
278
  def column_declarations(self, database_type: DbType) -> list[str]:
279
+ """Return a list of SQL column-declaration strings for the given DBMS."""
267
280
  # Returns a list of column specifications.
268
281
  spec = []
269
282
  for col in self.cols:
@@ -277,6 +290,7 @@ class DataTable:
277
290
  tablename: str,
278
291
  pretty: bool = False,
279
292
  ) -> str:
293
+ """Generate a CREATE TABLE statement for the given DBMS and table name."""
280
294
  tb = (
281
295
  f"{database_type.quoted(schemaname)}.{database_type.quoted(tablename)}"
282
296
  if schemaname
@@ -292,7 +306,10 @@ class DataTable:
292
306
 
293
307
 
294
308
  class JsonDatatype:
309
+ """Namespace mapping Python DataType subclasses to JSON Schema type strings."""
310
+
295
311
  def __init__(self) -> None:
312
+ """Create an empty JsonDatatype namespace instance."""
296
313
  pass
297
314
 
298
315
 
execsql/parser.py CHANGED
@@ -39,20 +39,26 @@ __all__ = [
39
39
 
40
40
 
41
41
  class SourceString:
42
+ """Cursor-based scanner over a raw string for token matching."""
43
+
42
44
  def __init__(self, source_string: str) -> None:
45
+ """Initialise the scanner at position zero of the given string."""
43
46
  self.str = source_string
44
47
  self.currpos = 0
45
48
 
46
49
  def eoi(self) -> bool:
50
+ """Return True if the entire source string has been consumed."""
47
51
  # Returns True or False indicating whether or not there is any of
48
52
  # the source string left to be consumed.
49
53
  return self.currpos >= len(self.str)
50
54
 
51
55
  def eat_whitespace(self) -> None:
56
+ """Advance the cursor past any whitespace at the current position."""
52
57
  while not self.eoi() and self.str[self.currpos] in [" ", "\t", "\n"]:
53
58
  self.currpos += 1
54
59
 
55
60
  def match_str(self, str: str) -> str | None:
61
+ """Match a string case-insensitively at the current position and advance."""
56
62
  # Tries to match the 'str' argument at the current position in the
57
63
  # source string. Matching is case-insensitive. If matching succeeds,
58
64
  # the matched string is returned and the internal pointer is incremented.
@@ -70,6 +76,7 @@ class SourceString:
70
76
  return None
71
77
 
72
78
  def match_regex(self, regex: Any) -> dict | None:
79
+ """Match a compiled regex at the current position and return named groups."""
73
80
  # Tries to match the 'regex' argument at the current position in the
74
81
  # source string. If it succeeds, a dictionary of all of the named
75
82
  # groups is returned, and the internal pointer is incremented.
@@ -85,6 +92,7 @@ class SourceString:
85
92
  return None
86
93
 
87
94
  def match_metacommand(self, commandlist: Any) -> tuple | None:
95
+ """Match a metacommand from the command list at the current position."""
88
96
  # Tries to match text at the current position to any metacommand
89
97
  # in the specified commandlist.
90
98
  # If it succeeds, the return value is a tuple of the MetaCommand object
@@ -102,6 +110,7 @@ class SourceString:
102
110
  return None
103
111
 
104
112
  def remainder(self) -> str:
113
+ """Return the unconsumed portion of the source string."""
105
114
  return self.str[self.currpos :]
106
115
 
107
116
 
@@ -109,10 +118,14 @@ class SourceString:
109
118
 
110
119
 
111
120
  class CondTokens:
121
+ """Integer constants for conditional-expression AST node types."""
122
+
112
123
  AND, OR, NOT, CONDITIONAL = range(4)
113
124
 
114
125
 
115
126
  class NumTokens:
127
+ """Integer constants for numeric-expression AST node types."""
128
+
116
129
  MUL, DIV, ADD, SUB, NUMBER = range(5)
117
130
 
118
131
 
@@ -120,7 +133,10 @@ class NumTokens:
120
133
 
121
134
 
122
135
  class CondAstNode(CondTokens):
136
+ """AST node for boolean expressions supporting AND, OR, NOT, and leaf conditionals."""
137
+
123
138
  def __init__(self, type: int, cond1: Any, cond2: Any) -> None:
139
+ """Create a conditional AST node with the given operator type and children."""
124
140
  # 'type' should be one of the constants AND, OR, NOT, CONDITIONAL.
125
141
  # For AND and OR types, 'cond1' and 'cond2' should be a subtree (a CondAstNode)
126
142
  # For NOT type, 'cond1' should be a CondAstNode and 'cond2' should be None
@@ -134,6 +150,7 @@ class CondAstNode(CondTokens):
134
150
  self.right = None
135
151
 
136
152
  def eval(self) -> bool:
153
+ """Evaluate this subtree and return a boolean result."""
137
154
  # Evaluates the subtrees and/or conditional value for this node,
138
155
  # returning True or False.
139
156
  if self.type == self.CONDITIONAL:
@@ -157,7 +174,10 @@ class CondAstNode(CondTokens):
157
174
 
158
175
 
159
176
  class NumericAstNode(NumTokens):
177
+ """AST node for arithmetic expressions supporting MUL, DIV, ADD, SUB, and NUMBER."""
178
+
160
179
  def __init__(self, type: int, value1: Any, value2: Any) -> None:
180
+ """Create a numeric AST node with the given operator type and operands."""
161
181
  # 'type' should be one of the constants MUL, DIV, ADD, SUB, OR NUMBER.
162
182
  # 'value1' and 'value2' should each be either a subtree (a
163
183
  # NumericAstNode) or (only 'value1' should be) a number.
@@ -169,6 +189,7 @@ class NumericAstNode(NumTokens):
169
189
  self.right = None
170
190
 
171
191
  def eval(self) -> Any:
192
+ """Evaluate this subtree and return a numeric result."""
172
193
  # Evaluates the subtrees and/or numeric value for this node,
173
194
  # returning a numeric value.
174
195
  if self.type == self.NUMBER:
@@ -190,12 +211,16 @@ class NumericAstNode(NumTokens):
190
211
 
191
212
 
192
213
  class CondParser(CondTokens):
214
+ """Recursive-descent parser for boolean conditional expressions."""
215
+
193
216
  # Takes a conditional expression string.
194
217
  def __init__(self, condexpr: str) -> None:
218
+ """Initialise the parser with the conditional expression string."""
195
219
  self.condexpr = condexpr
196
220
  self.cond_expr = SourceString(condexpr)
197
221
 
198
222
  def match_not(self) -> int | None:
223
+ """Match a NOT operator and return its token type, or None."""
199
224
  # Try to match 'NOT' operator. If not found, return None
200
225
  m1 = self.cond_expr.match_str("NOT")
201
226
  if m1 is not None:
@@ -203,6 +228,7 @@ class CondParser(CondTokens):
203
228
  return None
204
229
 
205
230
  def match_andop(self) -> int | None:
231
+ """Match an AND operator and return its token type, or None."""
206
232
  # Try to match 'AND' operator. If not found, return None
207
233
  m1 = self.cond_expr.match_str("AND")
208
234
  if m1 is not None:
@@ -210,6 +236,7 @@ class CondParser(CondTokens):
210
236
  return None
211
237
 
212
238
  def match_orop(self) -> int | None:
239
+ """Match an OR operator and return its token type, or None."""
213
240
  # Try to match 'OR' operator. If not found, return None
214
241
  m1 = self.cond_expr.match_str("OR")
215
242
  if m1 is not None:
@@ -217,6 +244,7 @@ class CondParser(CondTokens):
217
244
  return None
218
245
 
219
246
  def factor(self) -> Any:
247
+ """Parse a factor: NOT, a parenthesised expression, or a metacommand leaf."""
220
248
  m1 = self.match_not()
221
249
  if m1 is not None:
222
250
  m1 = self.factor()
@@ -244,6 +272,7 @@ class CondParser(CondTokens):
244
272
  )
245
273
 
246
274
  def term(self) -> Any:
275
+ """Parse a term: a factor optionally followed by AND and another term."""
247
276
  m1 = self.factor()
248
277
  andop = self.match_andop()
249
278
  if andop is not None:
@@ -253,6 +282,7 @@ class CondParser(CondTokens):
253
282
  return m1
254
283
 
255
284
  def expression(self) -> Any:
285
+ """Parse an expression: a term optionally followed by OR and another expression."""
256
286
  e1 = self.term()
257
287
  orop = self.match_orop()
258
288
  if orop is not None:
@@ -262,6 +292,7 @@ class CondParser(CondTokens):
262
292
  return e1
263
293
 
264
294
  def parse(self) -> Any:
295
+ """Parse the entire conditional expression and return the AST root."""
265
296
  exp = self.expression()
266
297
  if not self.cond_expr.eoi():
267
298
  raise CondParserError(
@@ -274,13 +305,17 @@ class CondParser(CondTokens):
274
305
 
275
306
 
276
307
  class NumericParser(NumTokens):
308
+ """Recursive-descent parser for arithmetic numeric expressions."""
309
+
277
310
  # Takes a numeric expression string
278
311
  def __init__(self, numexpr: str) -> None:
312
+ """Initialise the parser with the numeric expression string."""
279
313
  self.num_expr = SourceString(numexpr)
280
314
  self.rxint = re.compile(r"(?P<int_num>[+-]?[0-9]+)")
281
315
  self.rxfloat = re.compile(r"(?P<float_num>[+-]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.[0-9]*)))")
282
316
 
283
317
  def match_number(self) -> Any | None:
318
+ """Match a float or integer literal and return its numeric value, or None."""
284
319
  # Try to match a number in the source string.
285
320
  # Return it if matched, return None if unmatched.
286
321
  m1 = self.num_expr.match_regex(self.rxfloat)
@@ -293,6 +328,7 @@ class NumericParser(NumTokens):
293
328
  return None
294
329
 
295
330
  def match_mulop(self) -> int | None:
331
+ """Match a multiplication or division operator and return its token type, or None."""
296
332
  # Try to match a multiplication or division operator in the source string.
297
333
  # if found, return the matching operator type. If not found, return None.
298
334
  m1 = self.num_expr.match_str("*")
@@ -305,6 +341,7 @@ class NumericParser(NumTokens):
305
341
  return None
306
342
 
307
343
  def match_addop(self) -> int | None:
344
+ """Match an addition or subtraction operator and return its token type, or None."""
308
345
  # Try to match an addition or subtraction operator in the source string.
309
346
  # if found, return the matching operator type. If not found, return None.
310
347
  m1 = self.num_expr.match_str("+")
@@ -317,6 +354,7 @@ class NumericParser(NumTokens):
317
354
  return None
318
355
 
319
356
  def factor(self) -> Any:
357
+ """Parse a numeric factor: a number literal or a parenthesised expression."""
320
358
  # Parses a factor out of the source string and returns the
321
359
  # AST node that is created.
322
360
  m1 = self.match_number()
@@ -338,6 +376,7 @@ class NumericParser(NumTokens):
338
376
  )
339
377
 
340
378
  def term(self) -> Any:
379
+ """Parse a term: a factor optionally followed by MUL/DIV and another term."""
341
380
  # Parses a term out of the source string and returns the
342
381
  # AST node that is created.
343
382
  m1 = self.factor()
@@ -349,6 +388,7 @@ class NumericParser(NumTokens):
349
388
  return m1
350
389
 
351
390
  def expression(self) -> Any:
391
+ """Parse an expression: a term optionally followed by ADD/SUB and another expression."""
352
392
  # Parses an expression out of the source string and returns the
353
393
  # AST node that is created.
354
394
  e1 = self.term()
@@ -362,6 +402,7 @@ class NumericParser(NumTokens):
362
402
  return e1
363
403
 
364
404
  def parse(self) -> Any:
405
+ """Parse the entire numeric expression and return the AST root."""
365
406
  exp = self.expression()
366
407
  if not self.num_expr.eoi():
367
408
  raise NumericParserError(
execsql/script/control.py CHANGED
@@ -12,6 +12,8 @@ from typing import Any
12
12
 
13
13
  from execsql.exceptions import ErrInfo
14
14
 
15
+ __all__ = ["BatchLevels", "IfItem", "IfLevels"]
16
+
15
17
 
16
18
  # ---------------------------------------------------------------------------
17
19
  # BatchLevels
execsql/script/engine.py CHANGED
@@ -40,6 +40,25 @@ from execsql.script.variables import LocalSubVarSet, ScriptArgSubVarSet, SubVarS
40
40
  from execsql.utils.errors import exception_desc
41
41
  from execsql.utils.fileio import EncodedFile
42
42
 
43
+ __all__ = [
44
+ "MetaCommand",
45
+ "MetaCommandList",
46
+ "SqlStmt",
47
+ "MetacommandStmt",
48
+ "ScriptCmd",
49
+ "CommandList",
50
+ "CommandListWhileLoop",
51
+ "CommandListUntilLoop",
52
+ "ScriptFile",
53
+ "ScriptExecSpec",
54
+ "set_system_vars",
55
+ "substitute_vars",
56
+ "runscripts",
57
+ "current_script_line",
58
+ "read_sqlfile",
59
+ "read_sqlstring",
60
+ ]
61
+
43
62
 
44
63
  # ---------------------------------------------------------------------------
45
64
  # MetaCommand / MetaCommandList
@@ -15,6 +15,8 @@ from typing import Any
15
15
 
16
16
  from execsql.exceptions import ErrInfo
17
17
 
18
+ __all__ = ["CounterVars", "SubVarSet", "LocalSubVarSet", "ScriptArgSubVarSet"]
19
+
18
20
 
19
21
  # ---------------------------------------------------------------------------
20
22
  # CounterVars
@@ -184,15 +186,17 @@ class SubVarSet:
184
186
  return template_str.lower() in self._subs_dict
185
187
 
186
188
  def merge(self, other_subvars: SubVarSet | None) -> SubVarSet:
187
- """Return a new SubVarSet with this object's variables merged with other_subvars."""
189
+ """Return a new SubVarSet with this object's variables merged with other_subvars.
190
+
191
+ Copies dictionaries and pre-compiled patterns directly instead of
192
+ re-adding variables one at a time, avoiding O(V) regex recompilation.
193
+ """
188
194
  if other_subvars is not None:
189
195
  newsubs = SubVarSet()
190
- newsubs._subs_dict = dict(self._subs_dict)
191
- newsubs._compiled_patterns = dict(self._compiled_patterns)
196
+ newsubs._subs_dict = {**self._subs_dict, **other_subvars._subs_dict}
197
+ newsubs._compiled_patterns = {**self._compiled_patterns, **other_subvars._compiled_patterns}
192
198
  newsubs.prefix_list = list(set(self.prefix_list + other_subvars.prefix_list))
193
199
  newsubs.compile_var_rx()
194
- for varname, value in other_subvars._subs_dict.items():
195
- newsubs.add_substitution(varname, value)
196
200
  return newsubs
197
201
  return self
198
202