execsql2 2.18.0__py3-none-any.whl → 2.18.1__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 (27) hide show
  1. execsql/cli/__init__.py +3 -5
  2. execsql/cli/lint.py +433 -18
  3. execsql/metacommands/dispatch.py +5 -10
  4. execsql/metacommands/script_ext.py +8 -7
  5. execsql/script/engine.py +1 -12
  6. {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/METADATA +42 -40
  7. {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/RECORD +26 -27
  8. execsql/cli/lint_ast.py +0 -439
  9. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/README.md +0 -0
  10. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  11. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  12. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/execsql.conf +0 -0
  13. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  14. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  15. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  16. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  17. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  18. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  19. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  20. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/script_template.sql +0 -0
  21. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  22. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  23. {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  24. {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/WHEEL +0 -0
  25. {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/entry_points.txt +0 -0
  26. {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/licenses/LICENSE.txt +0 -0
  27. {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/licenses/NOTICE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.18.0
3
+ Version: 2.18.1
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
@@ -228,44 +228,46 @@ execsql script.sql # read connection from config file
228
228
 
229
229
  ## Options
230
230
 
231
- | Flag | Description |
232
- | ------------------------------------- | ----------------------------------------------------------------- |
233
- | `-t {p,m,s,l,k,a,f,o,d}` | Database type |
234
- | `-u USER` | Database username |
235
- | `-p PORT` | Server port |
236
- | `-a VALUE` | Set substitution variable `$ARG_x` |
237
- | `-b` / `--boolean-int` | Treat integers 0 and 1 as boolean values |
238
- | `-c SCRIPT` | Execute inline SQL or metacommand string |
239
- | `-d` | Auto-create export directories |
240
- | `-e ENCODING` / `--database-encoding` | Character encoding used in the database |
241
- | `-f ENCODING` | Script file encoding (default: UTF-8) |
242
- | `-g ENCODING` / `--output-encoding` | Encoding for WRITE and EXPORT output |
243
- | `-i ENCODING` / `--import-encoding` | Encoding for data files used with IMPORT |
244
- | `-l` | Write run log to `~/execsql.log` |
245
- | `-m` | List metacommands and exit |
246
- | `-n` | Create a new SQLite or PostgreSQL database if it does not exist |
247
- | `-o` / `--online-help` | Open the online documentation in the default browser |
248
- | `-s N` / `--scan-lines` | Lines to scan for IMPORT format detection (0 = scan entire file) |
249
- | `-v {0,1,2,3}` | GUI level (0=none, 1=password, 2=selection, 3=full) |
250
- | `-w` | Skip password prompt when a username is supplied |
251
- | `-y` / `--encodings` | List available encoding names and exit |
252
- | `-z KB` / `--import-buffer` | Import buffer size in KB (default: 32) |
253
- | `--dsn URL` | Connection string (e.g. `postgresql://user:pass@host/db`) |
254
- | `--output-dir DIR` | Default base directory for EXPORT output files |
255
- | `--dry-run` | Parse the script and report commands without executing |
256
- | `--lint` | Static analysis: check structure and warn on issues (no DB) |
257
- | `--parse-tree` | Print the script's AST structure and exit (no DB) |
258
- | `--list-plugins` | List discovered plugins and exit |
259
- | `--ping` | Test database connectivity and exit |
260
- | `--profile` | Show per-statement timing summary after execution |
261
- | `--profile-limit N` | Top N statements to display in `--profile` summary (default: 20) |
262
- | `--progress` | Show a progress bar for long-running IMPORT operations |
263
- | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
264
- | `--no-system-cmd` | Disable the `SYSTEM_CMD` metacommand (safer for CI / shared envs) |
265
- | `--init-config` | Print a default `execsql.conf` template to stdout and exit |
266
- | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
267
- | `--dump-keywords` | Print metacommand keywords as JSON and exit |
268
- | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
231
+ | Flag | Description |
232
+ | ------------------------------------- | ------------------------------------------------------------------ |
233
+ | `-t {p,m,s,l,k,a,f,o,d}` | Database type |
234
+ | `-u USER` | Database username |
235
+ | `-p PORT` | Server port |
236
+ | `-a VALUE` | Set substitution variable `$ARG_x` |
237
+ | `-b` / `--boolean-int` | Treat integers 0 and 1 as boolean values |
238
+ | `-c SCRIPT` | Execute inline SQL or metacommand string |
239
+ | `-d` | Auto-create export directories |
240
+ | `-e ENCODING` / `--database-encoding` | Character encoding used in the database |
241
+ | `-f ENCODING` | Script file encoding (default: UTF-8) |
242
+ | `-g ENCODING` / `--output-encoding` | Encoding for WRITE and EXPORT output |
243
+ | `-i ENCODING` / `--import-encoding` | Encoding for data files used with IMPORT |
244
+ | `-l` | Write run log to `~/execsql.log` |
245
+ | `-m` | List metacommands and exit |
246
+ | `-n` | Create a new SQLite or PostgreSQL database if it does not exist |
247
+ | `-o` / `--online-help` | Open the online documentation in the default browser |
248
+ | `-s N` / `--scan-lines` | Lines to scan for IMPORT format detection (0 = scan entire file) |
249
+ | `-v {0,1,2,3}` | GUI level (0=none, 1=password, 2=selection, 3=full) |
250
+ | `-w` | Skip password prompt when a username is supplied |
251
+ | `-y` / `--encodings` | List available encoding names and exit |
252
+ | `-z KB` / `--import-buffer` | Import buffer size in KB (default: 32) |
253
+ | `--dsn URL` | Connection string (e.g. `postgresql://user:pass@host/db`) |
254
+ | `--output-dir DIR` | Default base directory for EXPORT output files |
255
+ | `--dry-run` | Parse the script and report commands without executing |
256
+ | `--lint` | Static analysis: check structure and warn on issues (no DB) |
257
+ | `--parse-tree` | Print the script's AST structure and exit (no DB) |
258
+ | `--list-plugins` | List discovered plugins and exit |
259
+ | `--ping` | Test database connectivity and exit |
260
+ | `--profile` | Show per-statement timing summary after execution |
261
+ | `--profile-limit N` | Top N statements to display in `--profile` summary (default: 20) |
262
+ | `--progress` | Show a progress bar for long-running IMPORT operations |
263
+ | `--config FILE` | Load an explicit config file (highest priority after CLI args) |
264
+ | `--no-system-cmd` | Disable the `SYSTEM_CMD` metacommand (safer for CI / shared envs) |
265
+ | `--no-rm-file` | Disable the `RM_FILE` metacommand (no script-driven file deletion) |
266
+ | `--no-serve` | Disable the `SERVE` metacommand (no script-driven file streaming) |
267
+ | `--init-config` | Print a default `execsql.conf` template to stdout and exit |
268
+ | `--debug` | Start in step-through debug mode (REPL pauses before each stmt) |
269
+ | `--dump-keywords` | Print metacommand keywords as JSON and exit |
270
+ | `--gui-framework {tkinter,textual}` | GUI framework for interactive prompts |
269
271
 
270
272
  Run `execsql --help` for the full option list, or `execsql -m` to list all metacommands.
271
273
 
@@ -393,7 +395,7 @@ execsql-format --check scripts/
393
395
  ```yaml
394
396
  repos:
395
397
  - repo: https://github.com/geocoug/execsql
396
- rev: v2.11.0
398
+ rev: v2.18.0
397
399
  hooks:
398
400
  - id: execsql-format
399
401
  args: [--in-place]
@@ -10,11 +10,10 @@ execsql/plugins.py,sha256=2voLwT6eFap6BCBoZYndNNC_bMEJO1f_aP6xQTVXwYI,12815
10
10
  execsql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  execsql/state.py,sha256=T6UoXXxAkUP-4KKQpfFAlI3WMzm2xUi3LSplJDuRLY0,21965
12
12
  execsql/types.py,sha256=5K3aTuWQZHftz5slFODwqxlcvvt6lROISUnvUtRUazs,31799
13
- execsql/cli/__init__.py,sha256=km5f65aRHKZ1vLHRMnbZVXo-31gImxxOr1BvOIQg7B4,23226
13
+ 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
- execsql/cli/lint.py,sha256=xoOvFcoYFJpGjpgFD9CGgPAJmoJ_FPuyecBNBCazfJo,3459
17
- execsql/cli/lint_ast.py,sha256=c9UEFsZ7PZlFdrK0zJCe-WXfCqvS3WlOUWEycZozqB8,14688
16
+ execsql/cli/lint.py,sha256=YqKzFNUhyb_Th69hYgKk1ZZVjCsZfJMIiUGqp06JwNs,17236
18
17
  execsql/cli/run.py,sha256=QoSHVBfg20n2knPrqf7RFJmcfFpC5aq7NkwX5o6qRnA,36326
19
18
  execsql/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
19
  execsql/data/execsql.conf.template,sha256=1a2g2Vga7s128wcu3ftIFRkHlKKtuvkuOHSD1XuNT7o,9404
@@ -72,20 +71,20 @@ execsql/metacommands/connect.py,sha256=W24gYGmYDXNQyzBTsqWtl9-qbX2FS0v_c4s_OHj97
72
71
  execsql/metacommands/control.py,sha256=btF9hP_jzTuTIODPK72CYF0v_oKYpwXpKLATt-Ti2kc,7988
73
72
  execsql/metacommands/data.py,sha256=u9D6F3ambIqXhEHVmFOI6RDrbmdXQ-FUiqw7aMGo5bQ,12135
74
73
  execsql/metacommands/debug.py,sha256=MeVXAob8ItEg2QzuSUkKDaQCEABnH6u0XcAwJzw36CE,13015
75
- execsql/metacommands/dispatch.py,sha256=DvpgpkX1yMWnJrVW8ir_o1xKgfZgXzlTt6IwP0Lwoek,87441
74
+ execsql/metacommands/dispatch.py,sha256=t7x0xWHJA0PeCrYf7jYeSMJgf0yxcZ_xh-_YAtBPHLw,87192
76
75
  execsql/metacommands/io.py,sha256=vlGBje5sgnqeilooMdhJDgSRIhysHy5_7LrKtik9Xjs,3011
77
76
  execsql/metacommands/io_export.py,sha256=-7VDtUUQegwGRantw-NpdkI_9hwIKU-36ZvReIYC2QA,14285
78
77
  execsql/metacommands/io_fileops.py,sha256=6yED22UlVNXcRHNxZTgna8HmwFcR8s4nt6epMGLMtHY,10139
79
78
  execsql/metacommands/io_import.py,sha256=kUHIiI16WMuxXpqDMIRBc06KvI-_sYuYfJVNMTKOoAo,15706
80
79
  execsql/metacommands/io_write.py,sha256=RqOklmeGifu3lQgi_0glOoBWTY9FFIDlWIpOpb_hc3o,12789
81
80
  execsql/metacommands/prompt.py,sha256=LUl7doqKx8fLR0qt2lXbhdgNpdF1s6KZ6Ruitt10YnM,37483
82
- execsql/metacommands/script_ext.py,sha256=NShl29U9eI75_aL9WTSx8m4GjhCvMgJehzlES5M36cY,3693
81
+ execsql/metacommands/script_ext.py,sha256=CsUbyq8QFPnnvfX_k7fPlYNvvRw1xLmAWER4Exo5pwc,3658
83
82
  execsql/metacommands/system.py,sha256=5ETiWbkVDGsdwTldDaMM4rwY_Smd02wCWUROtf6XH_M,7385
84
83
  execsql/metacommands/upsert.py,sha256=wzoMpM8g49pEvU9GkHZ62fPvqV3w1UIUfxVA7HAsS_o,20317
85
84
  execsql/script/__init__.py,sha256=eGJPBDWj42aaId2lX_quSrqoKrvGwGElIrGDNCyoV1Y,3547
86
85
  execsql/script/ast.py,sha256=TQ4_7Lfw1F8_k6ycdvMZdzwNafrZiljSrthVRWUsuIk,20585
87
86
  execsql/script/control.py,sha256=WqLy-HLPqHG3vEzYpKMiIJsD7LpORjyQuUWzFzcGz4w,2327
88
- execsql/script/engine.py,sha256=woSTkoLOKaOpVqKzpAjPa0j7hs6ZpCFm172o6zJ_J3g,21363
87
+ execsql/script/engine.py,sha256=52RmtQJGk4KWqXpZY7jfKeiPojAoULHWaigOcm1azm4,20979
89
88
  execsql/script/executor.py,sha256=UTQ4k8EjxH5CdKYZ0E_l-0WyQ-i769mUJw6cJmMvSxI,36018
90
89
  execsql/script/parser.py,sha256=K-mgwuQ729KdmimOpEmb0OBzMyOvX3gxhBKLgr5P4VA,33697
91
90
  execsql/script/variables.py,sha256=t0BwrRuA8m1LYHGLkDPNbqW6QmudXroOFYsO0fwK2N0,16302
@@ -101,24 +100,24 @@ execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
101
100
  execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
102
101
  execsql/utils/strings.py,sha256=UQNjpRCEFa1UO6feU-M-9e24wWAvizs_iu_4fFusLxo,8516
103
102
  execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
104
- execsql2-2.18.0.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
105
- execsql2-2.18.0.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
106
- execsql2-2.18.0.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
107
- execsql2-2.18.0.data/data/execsql2_extras/execsql.conf,sha256=1a2g2Vga7s128wcu3ftIFRkHlKKtuvkuOHSD1XuNT7o,9404
108
- execsql2-2.18.0.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
109
- execsql2-2.18.0.data/data/execsql2_extras/md_compare.sql,sha256=qYYVAjSeHZzjszxV3Bv6bg8Ckbq2kMHl87_gh4sywMU,24140
110
- execsql2-2.18.0.data/data/execsql2_extras/md_glossary.sql,sha256=hkZ2Onn57LAKKsuXxzhR8tPtcWXkmWEQkwPE58-Tm2k,10796
111
- execsql2-2.18.0.data/data/execsql2_extras/md_upsert.sql,sha256=_CAK4BzEboRXTNy03SJR-oOjcEdSNMuRBPL6noWUptY,112560
112
- execsql2-2.18.0.data/data/execsql2_extras/pg_compare.sql,sha256=1zJd4hVUKHR0tncc2qTBC9B4qVV4Us2ITkJpsjN3tMw,24352
113
- execsql2-2.18.0.data/data/execsql2_extras/pg_glossary.sql,sha256=IKuwna-_8b20ljSkXZruuiQigrCpo7ueQdUqd1MXiuI,9908
114
- execsql2-2.18.0.data/data/execsql2_extras/pg_upsert.sql,sha256=HpPJtTHvpEjQy03j-3iPxDEOHMRkudOg7O4D4YR38UI,108315
115
- execsql2-2.18.0.data/data/execsql2_extras/script_template.sql,sha256=2J35ddZPguJ-vwTsz83wErv0jiWUyJcdW_JM0mNKDXA,11155
116
- execsql2-2.18.0.data/data/execsql2_extras/ss_compare.sql,sha256=j1qVNUPXQsEU7-DoVgDJCGcE0EuIl7whLBT3fgeiMAo,24833
117
- execsql2-2.18.0.data/data/execsql2_extras/ss_glossary.sql,sha256=2gLxv34xzKt0vy7hSzJH7a9JiMC3ETrv9MofxQwAibU,13065
118
- execsql2-2.18.0.data/data/execsql2_extras/ss_upsert.sql,sha256=G_8rQ0VzuKIZHWs24O_WrfzpC5S27R1JsL-bFBR3SUQ,117730
119
- execsql2-2.18.0.dist-info/METADATA,sha256=4ww8XW7Ujq-834kd1N2PMyF8YRmlNsz6Bb5TC_c2SEw,21651
120
- execsql2-2.18.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
121
- execsql2-2.18.0.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
122
- execsql2-2.18.0.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
123
- execsql2-2.18.0.dist-info/licenses/NOTICE,sha256=McYzgxYav3U1OaVsY4Su1sfBrfmplpRdA9b6-gCDQCg,342
124
- execsql2-2.18.0.dist-info/RECORD,,
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,,
execsql/cli/lint_ast.py DELETED
@@ -1,439 +0,0 @@
1
- """AST-based static analysis (lint) for execsql scripts.
2
-
3
- Performs the same checks as :mod:`execsql.cli.lint` but operates on the
4
- :class:`~execsql.script.ast.Script` tree instead of a flat
5
- :class:`~execsql.script.engine.CommandList`.
6
-
7
- Advantages over the flat linter:
8
-
9
- - **No runtime state required** — works with the AST parser alone, so it
10
- can run as an early exit in the CLI without initialising ``_state``.
11
- - **Structural validation is free** — the AST parser already rejects
12
- unmatched IF/LOOP/BATCH/SCRIPT blocks at parse time with precise source
13
- spans. This linter only needs to report variable and INCLUDE issues.
14
- - **Script blocks are in the tree** — ``EXECUTE SCRIPT`` targets are
15
- resolved by finding :class:`ScriptBlock` nodes, not by looking up
16
- ``_state.savedscripts``.
17
-
18
- Checks performed
19
- ----------------
20
- 1. **Parse errors** — the AST parser rejects unmatched blocks, so any
21
- parse failure is reported as an error with the parser's message.
22
- 2. **Potentially undefined variables** — same heuristic as the flat linter.
23
- 3. **EXECUTE SCRIPT target resolution** — warns when a target name does
24
- not correspond to a :class:`ScriptBlock` in the same file.
25
- 4. **Missing INCLUDE files** — warns when the file does not exist on disk.
26
- 5. **Empty script** — warns when no nodes were parsed.
27
- """
28
-
29
- from __future__ import annotations
30
-
31
- import re
32
- from pathlib import Path
33
-
34
- from execsql.script.ast import (
35
- BatchBlock,
36
- IfBlock,
37
- IncludeDirective,
38
- LoopBlock,
39
- MetaCommandStatement,
40
- Node,
41
- Script,
42
- ScriptBlock,
43
- SqlBlock,
44
- SqlStatement,
45
- )
46
-
47
- __all__ = ["lint_ast"]
48
-
49
-
50
- # ---------------------------------------------------------------------------
51
- # Variable-related patterns (shared with the flat linter)
52
- # ---------------------------------------------------------------------------
53
-
54
- _RX_SUB = re.compile(r"^\s*SUB\s+(?P<name>[+~]?\w+)\s+", re.I)
55
- _RX_SUB_EMPTY = re.compile(r"^\s*SUB_EMPTY\s+(?P<name>[+~]?\w+)\s*$", re.I)
56
- _RX_SUB_ADD = re.compile(r"^\s*SUB_ADD\s+(?P<name>[+~]?\w+)\s+", re.I)
57
- _RX_SUB_APPEND = re.compile(r"^\s*SUB_APPEND\s+(?P<name>[+~]?\w+)\s", re.I)
58
- _RX_SUBDATA = re.compile(r"^\s*SUBDATA\s+(?P<name>[+~]?\w+)\s+", re.I)
59
- _RX_SUB_INI = re.compile(
60
- r'^\s*SUB_INI\s+(?:FILE\s+)?(?:"(?P<qfile>[^"]+)"|(?P<file>\S+))'
61
- r"(?:\s+SECTION)?\s+(?P<section>\w+)\s*$",
62
- re.I,
63
- )
64
- _RX_SELECTSUB = re.compile(r"^\s*(?:SELECT_?SUB|PROMPT\s+SELECT_?SUB)\s+", re.I)
65
- _RX_SUB_LOCAL = re.compile(r"^\s*SUB_LOCAL\s+(?P<name>\w+)\s+", re.I)
66
- _RX_SUB_TEMPFILE = re.compile(r"^\s*SUB_TEMPFILE\s+(?P<name>\w+)\s", re.I)
67
- _RX_SUB_DECRYPT = re.compile(r"^\s*SUB_DECRYPT\s+(?P<name>\w+)\s+", re.I)
68
- _RX_SUB_ENCRYPT = re.compile(r"^\s*SUB_ENCRYPT\s+(?P<name>\w+)\s+", re.I)
69
- _RX_SUB_QUERYSTRING = re.compile(r"^\s*SUB_QUERYSTRING\s+(?P<name>\w+)\s+", re.I)
70
-
71
- _RX_VAR_REF = re.compile(r"!!([$@&~#+]?\w+)!!", re.I)
72
-
73
-
74
- # ---------------------------------------------------------------------------
75
- # Issue tuple helpers
76
- # ---------------------------------------------------------------------------
77
-
78
- _Issue = tuple[str, str, int, str] # (severity, source, line_no, message)
79
-
80
-
81
- def _error(source: str, line_no: int, message: str) -> _Issue:
82
- return ("error", source, line_no, message)
83
-
84
-
85
- def _warning(source: str, line_no: int, message: str) -> _Issue:
86
- return ("warning", source, line_no, message)
87
-
88
-
89
- # ---------------------------------------------------------------------------
90
- # Built-in variable discovery (reuse from flat linter)
91
- # ---------------------------------------------------------------------------
92
-
93
-
94
- def _discover_builtin_vars() -> frozenset[str]:
95
- """Scan the execsql package source for ``$VARNAME`` system variables."""
96
- import importlib.util
97
-
98
- _rx_add_sub = re.compile(r'(?:(?<!\w)add_substitution|(?<!\w)sv)\s*\(\s*["\'](\$\w+)["\']')
99
- _rx_lazy = re.compile(r'register_lazy\s*\(\s*["\'](\$\w+)["\']')
100
-
101
- names: set[str] = set()
102
-
103
- spec = importlib.util.find_spec("execsql")
104
- if spec is None or spec.submodule_search_locations is None:
105
- return frozenset(names)
106
-
107
- pkg_dir = Path(spec.submodule_search_locations[0])
108
- for src_file in pkg_dir.rglob("*.py"):
109
- try:
110
- text = src_file.read_text(encoding="utf-8")
111
- except OSError:
112
- continue
113
- for m in _rx_add_sub.finditer(text):
114
- names.add(m.group(1).lstrip("$").upper())
115
- for m in _rx_lazy.finditer(text):
116
- names.add(m.group(1).lstrip("$").upper())
117
-
118
- return frozenset(names)
119
-
120
-
121
- _BUILTIN_VARS: frozenset[str] | None = None
122
-
123
-
124
- def _get_builtin_vars() -> frozenset[str]:
125
- """Return the cached set of built-in variable names, discovering on first call."""
126
- global _BUILTIN_VARS
127
- if _BUILTIN_VARS is None:
128
- _BUILTIN_VARS = _discover_builtin_vars()
129
- return _BUILTIN_VARS
130
-
131
-
132
- # ---------------------------------------------------------------------------
133
- # AST walker helpers
134
- # ---------------------------------------------------------------------------
135
-
136
-
137
- def _collect_script_blocks(script: Script) -> dict[str, ScriptBlock]:
138
- """Build a name → ScriptBlock lookup from all ScriptBlock nodes in the tree."""
139
- blocks: dict[str, ScriptBlock] = {}
140
- for node in script.walk():
141
- if isinstance(node, ScriptBlock):
142
- blocks[node.name] = node
143
- return blocks
144
-
145
-
146
- def _collect_defined_vars_from_nodes(
147
- nodes: list[Node],
148
- script_blocks: dict[str, ScriptBlock],
149
- script_dir: Path | None,
150
- defined: set[str],
151
- visited: set[str] | None = None,
152
- ) -> None:
153
- """Walk nodes and collect variable definitions into *defined*."""
154
- if visited is None:
155
- visited = set()
156
-
157
- for node in nodes:
158
- if isinstance(node, MetaCommandStatement):
159
- _extract_var_definition(node.command, script_dir, defined)
160
-
161
- elif isinstance(node, IncludeDirective) and node.is_execute_script:
162
- target = node.target.lower()
163
- if target in script_blocks and target not in visited:
164
- visited.add(target)
165
- _collect_defined_vars_from_nodes(
166
- script_blocks[target].body,
167
- script_blocks,
168
- script_dir,
169
- defined,
170
- visited,
171
- )
172
-
173
- # Recurse into block children
174
- if isinstance(node, (IfBlock, LoopBlock, BatchBlock, ScriptBlock, SqlBlock)):
175
- _collect_defined_vars_from_nodes(
176
- list(node.children()),
177
- script_blocks,
178
- script_dir,
179
- defined,
180
- visited,
181
- )
182
-
183
-
184
- def _extract_var_definition(
185
- command: str,
186
- script_dir: Path | None,
187
- defined: set[str],
188
- ) -> None:
189
- """Extract variable name from a SUB-family metacommand into *defined*."""
190
- for rx in (
191
- _RX_SUB,
192
- _RX_SUB_EMPTY,
193
- _RX_SUB_ADD,
194
- _RX_SUB_APPEND,
195
- _RX_SUBDATA,
196
- _RX_SUB_LOCAL,
197
- _RX_SUB_TEMPFILE,
198
- _RX_SUB_DECRYPT,
199
- _RX_SUB_ENCRYPT,
200
- _RX_SUB_QUERYSTRING,
201
- ):
202
- m = rx.match(command)
203
- if m:
204
- defined.add(m.group("name").lstrip("+~").upper())
205
- return
206
-
207
- # SUB_INI bulk-defines from INI file — read keys at lint time
208
- ini_m = _RX_SUB_INI.match(command)
209
- if ini_m:
210
- ini_file = ini_m.group("qfile") or ini_m.group("file")
211
- ini_section = ini_m.group("section")
212
- if ini_file and not _RX_VAR_REF.search(ini_file):
213
- _read_ini_vars(ini_file, ini_section, script_dir, defined)
214
-
215
-
216
- def _read_ini_vars(
217
- ini_file: str,
218
- section: str,
219
- script_dir: Path | None,
220
- defined_vars: set[str],
221
- ) -> None:
222
- """Read an INI file and register its section keys as defined variables."""
223
- from configparser import ConfigParser
224
-
225
- p = Path(ini_file)
226
- if not p.is_absolute() and script_dir is not None:
227
- p = script_dir / p
228
-
229
- if not p.exists():
230
- return
231
-
232
- cp = ConfigParser()
233
- cp.read(p)
234
- if cp.has_section(section):
235
- for key, _value in cp.items(section):
236
- defined_vars.add(key.upper())
237
-
238
-
239
- def _check_var_ref(
240
- raw_name: str,
241
- source: str,
242
- line_no: int,
243
- defined_vars: set[str],
244
- issues: list[_Issue],
245
- ) -> None:
246
- """Emit a warning if *raw_name* looks like an undefined user variable."""
247
- if not raw_name:
248
- return
249
-
250
- sigil = raw_name[0] if raw_name[0] in ("$", "@", "&", "~", "#", "+") else ""
251
- name = raw_name[len(sigil) :]
252
-
253
- # Skip non-$ sigil prefixes — resolved at runtime
254
- if sigil in ("@", "&", "~", "#", "+"):
255
- return
256
-
257
- # $ARG_N is set via -a/--assign-arg at invocation time
258
- if re.match(r"^ARG_\d+$", name, re.I):
259
- return
260
-
261
- # $COUNTER_N is managed by CounterVars
262
- if re.match(r"^COUNTER_\d+$", name, re.I):
263
- return
264
-
265
- # Built-in system variables
266
- if name.upper() in _get_builtin_vars():
267
- return
268
-
269
- # User-defined via SUB
270
- if name.upper() in defined_vars:
271
- return
272
-
273
- issues.append(
274
- _warning(
275
- source,
276
- line_no,
277
- f"Potentially undefined variable: !!{raw_name}!! "
278
- "(not defined by a preceding SUB; may be set by a config file or -a arg)",
279
- ),
280
- )
281
-
282
-
283
- def _check_include_path(
284
- raw_path: str,
285
- script_dir: Path | None,
286
- source: str,
287
- line_no: int,
288
- issues: list[_Issue],
289
- ) -> None:
290
- """Warn if the INCLUDE target does not exist on disk."""
291
- p = Path(raw_path)
292
- if not p.is_absolute() and script_dir is not None:
293
- p = script_dir / p
294
-
295
- if not p.exists():
296
- issues.append(
297
- _warning(source, line_no, f"INCLUDE target does not exist: {raw_path!r}"),
298
- )
299
-
300
-
301
- # ---------------------------------------------------------------------------
302
- # Core lint walk
303
- # ---------------------------------------------------------------------------
304
-
305
-
306
- def _lint_nodes(
307
- nodes: list[Node],
308
- script_dir: Path | None,
309
- defined_vars: set[str],
310
- script_blocks: dict[str, ScriptBlock],
311
- issues: list[_Issue],
312
- *,
313
- visited_scripts: set[str] | None = None,
314
- ) -> None:
315
- """Walk a list of AST nodes and collect lint issues."""
316
- if visited_scripts is None:
317
- visited_scripts = set()
318
-
319
- for node in nodes:
320
- src = node.span.file
321
- lno = node.span.start_line
322
-
323
- # -- Variable references in SQL --
324
- if isinstance(node, SqlStatement):
325
- for m in _RX_VAR_REF.finditer(node.text):
326
- _check_var_ref(m.group(1), src, lno, defined_vars, issues)
327
-
328
- # -- Metacommand checks --
329
- elif isinstance(node, MetaCommandStatement):
330
- for m in _RX_VAR_REF.finditer(node.command):
331
- _check_var_ref(m.group(1), src, lno, defined_vars, issues)
332
-
333
- # -- IncludeDirective checks --
334
- elif isinstance(node, IncludeDirective):
335
- if node.is_execute_script:
336
- target = node.target.lower()
337
- if target not in script_blocks:
338
- if not node.if_exists:
339
- issues.append(
340
- _warning(src, lno, f"EXECUTE SCRIPT target not found: '{target}'"),
341
- )
342
- elif target not in visited_scripts:
343
- visited_scripts.add(target)
344
- _lint_nodes(
345
- script_blocks[target].body,
346
- script_dir,
347
- defined_vars,
348
- script_blocks,
349
- issues,
350
- visited_scripts=visited_scripts,
351
- )
352
- else:
353
- # INCLUDE file existence check
354
- if not node.if_exists:
355
- raw_path = node.target.strip().strip("\"'")
356
- if not _RX_VAR_REF.search(raw_path):
357
- _check_include_path(raw_path, script_dir, src, lno, issues)
358
-
359
- # -- Recurse into block children --
360
- if isinstance(node, IfBlock):
361
- _lint_nodes(node.body, script_dir, defined_vars, script_blocks, issues, visited_scripts=visited_scripts)
362
- for clause in node.elseif_clauses:
363
- _lint_nodes(
364
- clause.body,
365
- script_dir,
366
- defined_vars,
367
- script_blocks,
368
- issues,
369
- visited_scripts=visited_scripts,
370
- )
371
- _lint_nodes(
372
- node.else_body,
373
- script_dir,
374
- defined_vars,
375
- script_blocks,
376
- issues,
377
- visited_scripts=visited_scripts,
378
- )
379
- elif isinstance(node, (LoopBlock, BatchBlock, SqlBlock)):
380
- _lint_nodes(node.body, script_dir, defined_vars, script_blocks, issues, visited_scripts=visited_scripts)
381
- elif isinstance(node, ScriptBlock):
382
- # Lint script block body (structural errors already caught by parser)
383
- if node.name not in visited_scripts:
384
- visited_scripts.add(node.name)
385
- sub_issues: list[_Issue] = []
386
- _lint_nodes(
387
- node.body,
388
- script_dir,
389
- defined_vars,
390
- script_blocks,
391
- sub_issues,
392
- visited_scripts=visited_scripts,
393
- )
394
- for sev, ssrc, slno, msg in sub_issues:
395
- issues.append((sev, ssrc, slno, f"[script '{node.name}'] {msg}"))
396
-
397
-
398
- # ---------------------------------------------------------------------------
399
- # Public API
400
- # ---------------------------------------------------------------------------
401
-
402
-
403
- def lint_ast(
404
- script: Script,
405
- script_path: str | None = None,
406
- ) -> list[_Issue]:
407
- """Perform static analysis on an AST-parsed script.
408
-
409
- Args:
410
- script: The parsed :class:`Script` tree.
411
- script_path: Path to the source file (for resolving relative
412
- INCLUDE paths). ``None`` for inline scripts.
413
-
414
- Returns:
415
- List of ``(severity, source, line_no, message)`` issue tuples.
416
- """
417
- issues: list[_Issue] = []
418
-
419
- if not script.body:
420
- issues.append(_warning("<script>", 0, "Script is empty — no commands found"))
421
- return issues
422
-
423
- script_dir = Path(script_path).resolve().parent if script_path else None
424
- script_blocks = _collect_script_blocks(script)
425
-
426
- # Pass 1: collect all variable definitions
427
- all_defined: set[str] = set()
428
- _collect_defined_vars_from_nodes(script.body, script_blocks, script_dir, all_defined)
429
-
430
- # Pass 2: lint for variable and include issues
431
- _lint_nodes(
432
- script.body,
433
- script_dir,
434
- all_defined,
435
- script_blocks,
436
- issues,
437
- )
438
-
439
- return issues