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.
- execsql/cli/__init__.py +3 -5
- execsql/cli/lint.py +433 -18
- execsql/metacommands/dispatch.py +5 -10
- execsql/metacommands/script_ext.py +8 -7
- execsql/script/engine.py +1 -12
- {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/METADATA +42 -40
- {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/RECORD +26 -27
- execsql/cli/lint_ast.py +0 -439
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/WHEEL +0 -0
- {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.18.0.dist-info → execsql2-2.18.1.dist-info}/licenses/LICENSE.txt +0 -0
- {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.
|
|
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
|
-
| `--
|
|
266
|
-
| `--
|
|
267
|
-
| `--
|
|
268
|
-
| `--
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
105
|
-
execsql2-2.18.
|
|
106
|
-
execsql2-2.18.
|
|
107
|
-
execsql2-2.18.
|
|
108
|
-
execsql2-2.18.
|
|
109
|
-
execsql2-2.18.
|
|
110
|
-
execsql2-2.18.
|
|
111
|
-
execsql2-2.18.
|
|
112
|
-
execsql2-2.18.
|
|
113
|
-
execsql2-2.18.
|
|
114
|
-
execsql2-2.18.
|
|
115
|
-
execsql2-2.18.
|
|
116
|
-
execsql2-2.18.
|
|
117
|
-
execsql2-2.18.
|
|
118
|
-
execsql2-2.18.
|
|
119
|
-
execsql2-2.18.
|
|
120
|
-
execsql2-2.18.
|
|
121
|
-
execsql2-2.18.
|
|
122
|
-
execsql2-2.18.
|
|
123
|
-
execsql2-2.18.
|
|
124
|
-
execsql2-2.18.
|
|
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
|
|
File without changes
|
|
File without changes
|
{execsql2-2.18.0.data → execsql2-2.18.1.data}/data/execsql2_extras/example_config_prompt.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|