execsql2 2.16.0__py3-none-any.whl → 2.16.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execsql/cli/run.py +333 -173
- execsql/config.py +1 -1
- execsql/script/executor.py +145 -47
- execsql/script/parser.py +9 -1
- execsql/state.py +72 -51
- {execsql2-2.16.0.dist-info → execsql2-2.16.2.dist-info}/METADATA +38 -29
- {execsql2-2.16.0.dist-info → execsql2-2.16.2.dist-info}/RECORD +26 -26
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.16.0.data → execsql2-2.16.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.16.0.dist-info → execsql2-2.16.2.dist-info}/WHEEL +0 -0
- {execsql2-2.16.0.dist-info → execsql2-2.16.2.dist-info}/entry_points.txt +0 -0
- {execsql2-2.16.0.dist-info → execsql2-2.16.2.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.16.0.dist-info → execsql2-2.16.2.dist-info}/licenses/NOTICE +0 -0
execsql/cli/run.py
CHANGED
|
@@ -192,138 +192,151 @@ def _ping_db(db: Any) -> None:
|
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
# ---------------------------------------------------------------------------
|
|
195
|
-
#
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# Extracted phases — pure computation, no global state coupling
|
|
196
197
|
# ---------------------------------------------------------------------------
|
|
197
198
|
|
|
198
199
|
|
|
199
|
-
def
|
|
200
|
-
|
|
201
|
-
sub_vars: list[str] | None,
|
|
202
|
-
boolean_int: str | None,
|
|
203
|
-
make_dirs: str | None,
|
|
204
|
-
database_encoding: str | None,
|
|
205
|
-
script_encoding: str | None,
|
|
206
|
-
output_encoding: str | None,
|
|
207
|
-
import_encoding: str | None,
|
|
208
|
-
user_logfile: bool,
|
|
209
|
-
new_db: bool,
|
|
210
|
-
port: int | None,
|
|
211
|
-
scanlines: int | None,
|
|
212
|
-
db_type: str | None,
|
|
213
|
-
user: str | None,
|
|
214
|
-
use_gui: str | None,
|
|
215
|
-
gui_framework: str | None = None,
|
|
216
|
-
no_passwd: bool = False,
|
|
217
|
-
import_buffer: int | None = None,
|
|
218
|
-
script_name: str | None = None,
|
|
219
|
-
command: str | None = None,
|
|
220
|
-
dry_run: bool = False,
|
|
221
|
-
dsn: str | None = None,
|
|
222
|
-
output_dir: str | None = None,
|
|
223
|
-
progress: bool = False,
|
|
224
|
-
profile: bool = False,
|
|
225
|
-
profile_limit: int = 20,
|
|
226
|
-
ping: bool = False,
|
|
227
|
-
lint: bool = False,
|
|
228
|
-
debug: bool = False,
|
|
229
|
-
config_file: str | None = None,
|
|
230
|
-
) -> None:
|
|
231
|
-
"""Initialise state, connect to the database, load the script, and run it.
|
|
232
|
-
|
|
233
|
-
Separated from argument parsing so it can be called directly in tests
|
|
234
|
-
without going through the Typer CLI layer. All parameters mirror the
|
|
235
|
-
corresponding CLI options; see [Syntax & Options](../syntax.md) for
|
|
236
|
-
descriptions.
|
|
237
|
-
|
|
238
|
-
When *ping* is ``True``, the function connects to the database, prints
|
|
239
|
-
connection details (DBMS name, server version, and location), and calls
|
|
240
|
-
:func:`_ping_db` which raises ``SystemExit(0)``. No script is loaded or
|
|
241
|
-
executed. *script_name* and *command* may both be ``None`` in ping mode.
|
|
200
|
+
def _seed_early_subvars() -> SubVarSet:
|
|
201
|
+
"""Create and populate the initial substitution variable pool.
|
|
242
202
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
undefined variables, missing INCLUDE files, empty scripts) without
|
|
246
|
-
connecting to a database or executing anything. Exits with code 0 if no
|
|
247
|
-
errors were found, or code 1 if errors were found.
|
|
203
|
+
Seeds environment variables (filtering secrets), timestamps, system info,
|
|
204
|
+
and placeholder variables. Called once at the start of :func:`_run`.
|
|
248
205
|
"""
|
|
249
|
-
|
|
206
|
+
subvars = SubVarSet()
|
|
250
207
|
|
|
251
|
-
# ------------------------------------------------------------------
|
|
252
|
-
# Early setup: substitution variables seeded before arg parsing
|
|
253
|
-
# ------------------------------------------------------------------
|
|
254
|
-
_state.subvars = SubVarSet()
|
|
255
|
-
|
|
256
|
-
# Environment variables are exposed as &-prefixed substitution variables.
|
|
257
|
-
# Variables whose names contain common secret-indicating substrings are
|
|
258
|
-
# excluded to reduce accidental credential leakage into scripts and logs.
|
|
259
208
|
_SENSITIVE_SUBSTRINGS = ("SECRET", "TOKEN", "PASSWORD", "PASSWD", "PRIVATE_KEY", "CREDENTIAL")
|
|
260
209
|
for k in os.environ:
|
|
261
210
|
if any(s in k.upper() for s in _SENSITIVE_SUBSTRINGS):
|
|
262
211
|
continue
|
|
263
212
|
try:
|
|
264
|
-
|
|
213
|
+
subvars.add_substitution("&" + k, os.environ[k])
|
|
265
214
|
except Exception:
|
|
266
215
|
pass # Skip env vars with names that can't be substitution keys.
|
|
267
|
-
|
|
216
|
+
subvars.add_substitution("$LAST_ROWCOUNT", None)
|
|
268
217
|
|
|
269
218
|
dt_now = datetime.datetime.now()
|
|
270
219
|
dt_now_utc = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
271
220
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
221
|
+
subvars.add_substitution("$SCRIPT_START_TIME", dt_now.strftime("%Y-%m-%d %H:%M"))
|
|
222
|
+
subvars.add_substitution("$SCRIPT_START_TIME_UTC", dt_now_utc.strftime("%Y-%m-%d %H:%M"))
|
|
223
|
+
subvars.add_substitution("$DATE_TAG", dt_now.strftime("%Y%m%d"))
|
|
224
|
+
subvars.add_substitution("$DATETIME_TAG", dt_now.strftime("%Y%m%d_%H%M"))
|
|
225
|
+
subvars.add_substitution("$DATETIME_UTC_TAG", dt_now_utc.strftime("%Y%m%d_%H%M"))
|
|
226
|
+
subvars.add_substitution("$LAST_SQL", "")
|
|
227
|
+
subvars.add_substitution("$LAST_ERROR", "")
|
|
228
|
+
subvars.add_substitution("$ERROR_MESSAGE", "")
|
|
229
|
+
subvars.add_substitution("$USER", getpass.getuser())
|
|
230
|
+
subvars.add_substitution("$STARTING_PATH", os.getcwd() + os.sep)
|
|
231
|
+
subvars.add_substitution("$PATHSEP", os.sep)
|
|
283
232
|
osys = sys.platform
|
|
284
233
|
if osys.startswith("linux"):
|
|
285
234
|
osys = "linux"
|
|
286
235
|
elif osys.startswith("win"):
|
|
287
236
|
osys = "windows"
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
237
|
+
subvars.add_substitution("$OS", osys)
|
|
238
|
+
subvars.add_substitution("$HOSTNAME", platform.node())
|
|
239
|
+
subvars.add_substitution("$PYTHON_EXECUTABLE", sys.executable)
|
|
240
|
+
return subvars
|
|
291
241
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
242
|
+
|
|
243
|
+
def _load_config(
|
|
244
|
+
script_name: str | None,
|
|
245
|
+
subvars: SubVarSet,
|
|
246
|
+
config_file: str | None,
|
|
247
|
+
) -> ConfigData:
|
|
248
|
+
"""Load and merge configuration files for the given script.
|
|
249
|
+
|
|
250
|
+
Returns a fully populated :class:`ConfigData` instance.
|
|
251
|
+
"""
|
|
295
252
|
script_path = str(Path(script_name).resolve().parent) if script_name else os.getcwd()
|
|
296
|
-
|
|
297
|
-
conf = _state.conf
|
|
253
|
+
return ConfigData(script_path, subvars, config_file=config_file)
|
|
298
254
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
255
|
+
|
|
256
|
+
def _seed_script_subvars(subvars: SubVarSet, script_name: str | None) -> None:
|
|
257
|
+
"""Add substitution variables that depend on the script path."""
|
|
258
|
+
from execsql.utils.errors import file_size_date
|
|
259
|
+
|
|
260
|
+
if script_name is not None:
|
|
261
|
+
subvars.add_substitution("$STARTING_SCRIPT", script_name)
|
|
262
|
+
subvars.add_substitution("$STARTING_SCRIPT_NAME", Path(script_name).name)
|
|
263
|
+
subvars.add_substitution("$STARTING_SCRIPT_REVTIME", file_size_date(script_name)[1])
|
|
264
|
+
else:
|
|
265
|
+
subvars.add_substitution("$STARTING_SCRIPT", "<inline>")
|
|
266
|
+
subvars.add_substitution("$STARTING_SCRIPT_NAME", "<inline>")
|
|
267
|
+
subvars.add_substitution("$STARTING_SCRIPT_REVTIME", "")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _load_script(
|
|
271
|
+
command: str | None,
|
|
272
|
+
script_name: str | None,
|
|
273
|
+
encoding: str,
|
|
274
|
+
) -> Any:
|
|
275
|
+
"""Parse the SQL script (or inline command) into an AST.
|
|
276
|
+
|
|
277
|
+
Returns the AST tree, or ``None`` if no script is provided.
|
|
278
|
+
"""
|
|
279
|
+
from execsql.script.parser import parse_script, parse_string
|
|
280
|
+
|
|
281
|
+
if command is not None:
|
|
282
|
+
return parse_string(
|
|
283
|
+
command.replace("\\n", "\n").replace("\\t", "\t"),
|
|
284
|
+
"<inline>",
|
|
285
|
+
)
|
|
286
|
+
return parse_script(script_name, encoding=encoding)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _apply_dsn(dsn: str, conf: ConfigData, db_type: str | None) -> tuple[str | None, str | None, int | None]:
|
|
290
|
+
"""Parse a DSN connection string and apply overrides to *conf*.
|
|
291
|
+
|
|
292
|
+
Returns ``(db_type, user_override, port_override)`` — values extracted
|
|
293
|
+
from the DSN that must also be applied to CLI-level variables in the
|
|
294
|
+
caller.
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
parsed = _parse_connection_string(dsn)
|
|
298
|
+
except ConfigError as exc:
|
|
299
|
+
_err_console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
300
|
+
raise SystemExit(1) from None
|
|
301
|
+
db_type = db_type or parsed["db_type"]
|
|
302
|
+
conf.db_type = db_type
|
|
303
|
+
if parsed["server"]:
|
|
304
|
+
conf.server = parsed["server"]
|
|
305
|
+
if parsed["db"]:
|
|
306
|
+
conf.db = parsed["db"]
|
|
307
|
+
if parsed["db_file"]:
|
|
308
|
+
conf.db_file = parsed["db_file"]
|
|
309
|
+
user = parsed["user"] # may be None
|
|
310
|
+
if parsed["password"]:
|
|
311
|
+
conf.db_password = parsed["password"]
|
|
312
|
+
conf.passwd_prompt = False
|
|
313
|
+
port = parsed["port"] # may be None
|
|
314
|
+
return db_type, user, port
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _apply_cli_options(
|
|
318
|
+
conf: ConfigData,
|
|
319
|
+
*,
|
|
320
|
+
user: str | None,
|
|
321
|
+
no_passwd: bool,
|
|
322
|
+
database_encoding: str | None,
|
|
323
|
+
script_encoding: str | None,
|
|
324
|
+
output_encoding: str | None,
|
|
325
|
+
import_encoding: str | None,
|
|
326
|
+
import_buffer: int | None,
|
|
327
|
+
make_dirs: str | None,
|
|
328
|
+
boolean_int: str | None,
|
|
329
|
+
scanlines: int | None,
|
|
330
|
+
use_gui: str | None,
|
|
331
|
+
gui_framework: str | None,
|
|
332
|
+
db_type: str | None,
|
|
333
|
+
user_logfile: bool,
|
|
334
|
+
port: int | None,
|
|
335
|
+
new_db: bool,
|
|
336
|
+
output_dir: str | None,
|
|
337
|
+
progress: bool,
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Merge CLI flags into *conf*, overriding config-file values."""
|
|
327
340
|
if user:
|
|
328
341
|
conf.username = user
|
|
329
342
|
if no_passwd:
|
|
@@ -363,7 +376,7 @@ def _run(
|
|
|
363
376
|
if db_type:
|
|
364
377
|
conf.db_type = db_type
|
|
365
378
|
if conf.db_type is None:
|
|
366
|
-
conf.db_type = "
|
|
379
|
+
conf.db_type = "l"
|
|
367
380
|
if user_logfile:
|
|
368
381
|
conf.user_logfile = True
|
|
369
382
|
if port:
|
|
@@ -377,9 +390,15 @@ def _run(
|
|
|
377
390
|
if progress:
|
|
378
391
|
conf.show_progress = True
|
|
379
392
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
393
|
+
|
|
394
|
+
def _route_positionals(
|
|
395
|
+
positional: list,
|
|
396
|
+
conf: ConfigData,
|
|
397
|
+
*,
|
|
398
|
+
command: str | None,
|
|
399
|
+
ping: bool,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Apply remaining positional CLI arguments to *conf* as server/db/db_file."""
|
|
383
402
|
off = 0 if (command is not None or ping) else 1
|
|
384
403
|
if len(positional) == off + 1:
|
|
385
404
|
if conf.db_type in ("a", "l", "k"):
|
|
@@ -399,50 +418,32 @@ def _run(
|
|
|
399
418
|
|
|
400
419
|
fatal_error("Incorrect number of command-line arguments.")
|
|
401
420
|
|
|
402
|
-
# ------------------------------------------------------------------
|
|
403
|
-
# Script substitution variables that depend on the script path
|
|
404
|
-
# ------------------------------------------------------------------
|
|
405
|
-
from execsql.utils.errors import file_size_date
|
|
406
|
-
|
|
407
|
-
if script_name is not None:
|
|
408
|
-
_state.subvars.add_substitution("$STARTING_SCRIPT", script_name)
|
|
409
|
-
_state.subvars.add_substitution("$STARTING_SCRIPT_NAME", Path(script_name).name)
|
|
410
|
-
_state.subvars.add_substitution("$STARTING_SCRIPT_REVTIME", file_size_date(script_name)[1])
|
|
411
|
-
else:
|
|
412
|
-
_state.subvars.add_substitution("$STARTING_SCRIPT", "<inline>")
|
|
413
|
-
_state.subvars.add_substitution("$STARTING_SCRIPT_NAME", "<inline>")
|
|
414
|
-
_state.subvars.add_substitution("$STARTING_SCRIPT_REVTIME", "")
|
|
415
|
-
|
|
416
|
-
# ------------------------------------------------------------------
|
|
417
|
-
# Initialise state objects
|
|
418
|
-
# ------------------------------------------------------------------
|
|
419
|
-
from execsql.metacommands import DISPATCH_TABLE
|
|
420
|
-
from execsql.metacommands.conditions import CONDITIONAL_TABLE
|
|
421
|
-
|
|
422
|
-
_state.initialize(conf, DISPATCH_TABLE, CONDITIONAL_TABLE)
|
|
423
421
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
422
|
+
def _setup_logging(
|
|
423
|
+
conf: ConfigData,
|
|
424
|
+
subvars: SubVarSet,
|
|
425
|
+
script_name: str | None,
|
|
426
|
+
sub_vars: list[str] | None,
|
|
427
|
+
*,
|
|
428
|
+
boolean_int: str | None,
|
|
429
|
+
make_dirs: str | None,
|
|
430
|
+
database_encoding: str | None,
|
|
431
|
+
script_encoding: str | None,
|
|
432
|
+
output_encoding: str | None,
|
|
433
|
+
import_encoding: str | None,
|
|
434
|
+
user_logfile: bool,
|
|
435
|
+
new_db: bool,
|
|
436
|
+
port: int | None,
|
|
437
|
+
scanlines: int | None,
|
|
438
|
+
db_type: str | None,
|
|
439
|
+
user: str | None,
|
|
440
|
+
use_gui: str | None,
|
|
441
|
+
no_passwd: bool,
|
|
442
|
+
import_buffer: int | None,
|
|
443
|
+
) -> Logger:
|
|
444
|
+
"""Create the execution logger, log initial info, and seed ``$RUN_ID``."""
|
|
445
|
+
from execsql.utils.errors import file_size_date
|
|
442
446
|
|
|
443
|
-
# ------------------------------------------------------------------
|
|
444
|
-
# Logging
|
|
445
|
-
# ------------------------------------------------------------------
|
|
446
447
|
opts_dict = {
|
|
447
448
|
k: v
|
|
448
449
|
for k, v in {
|
|
@@ -465,48 +466,207 @@ def _run(
|
|
|
465
466
|
}.items()
|
|
466
467
|
if v
|
|
467
468
|
}
|
|
468
|
-
|
|
469
|
+
logger = Logger(
|
|
469
470
|
script_name or "<inline>",
|
|
470
471
|
conf.db,
|
|
471
472
|
conf.server,
|
|
472
473
|
opts_dict,
|
|
473
474
|
conf.user_logfile,
|
|
474
475
|
)
|
|
475
|
-
|
|
476
|
+
logger.log_status_info(
|
|
476
477
|
f"Python version {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} {sys.version_info[3]}",
|
|
477
478
|
)
|
|
478
|
-
|
|
479
|
-
|
|
479
|
+
logger.log_status_info(f"execsql version {__version__}")
|
|
480
|
+
logger.log_status_info(f"System user: {getpass.getuser()}")
|
|
480
481
|
for configfile in conf.files_read:
|
|
481
482
|
sz, dt = file_size_date(configfile)
|
|
482
|
-
|
|
483
|
+
logger.log_status_info(
|
|
483
484
|
f"Read configuration file {configfile} (size: {sz}, date: {dt}).",
|
|
484
485
|
)
|
|
485
486
|
|
|
486
|
-
|
|
487
|
+
subvars.add_substitution("$RUN_ID", logger.run_id)
|
|
487
488
|
|
|
488
489
|
if sub_vars:
|
|
489
490
|
for n, repl in enumerate(sub_vars):
|
|
490
491
|
var = f"$ARG_{n + 1}"
|
|
491
|
-
|
|
492
|
-
|
|
492
|
+
subvars.add_substitution(var, repl)
|
|
493
|
+
logger.log_status_info(
|
|
493
494
|
f"Command-line substitution variable assignment: {var} set to {{{repl}}}",
|
|
494
495
|
)
|
|
495
496
|
|
|
497
|
+
return logger
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# ---------------------------------------------------------------------------
|
|
501
|
+
# Core execution (split from argument parsing for testability)
|
|
502
|
+
# ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _run(
|
|
506
|
+
positional: list,
|
|
507
|
+
sub_vars: list[str] | None,
|
|
508
|
+
boolean_int: str | None,
|
|
509
|
+
make_dirs: str | None,
|
|
510
|
+
database_encoding: str | None,
|
|
511
|
+
script_encoding: str | None,
|
|
512
|
+
output_encoding: str | None,
|
|
513
|
+
import_encoding: str | None,
|
|
514
|
+
user_logfile: bool,
|
|
515
|
+
new_db: bool,
|
|
516
|
+
port: int | None,
|
|
517
|
+
scanlines: int | None,
|
|
518
|
+
db_type: str | None,
|
|
519
|
+
user: str | None,
|
|
520
|
+
use_gui: str | None,
|
|
521
|
+
gui_framework: str | None = None,
|
|
522
|
+
no_passwd: bool = False,
|
|
523
|
+
import_buffer: int | None = None,
|
|
524
|
+
script_name: str | None = None,
|
|
525
|
+
command: str | None = None,
|
|
526
|
+
dry_run: bool = False,
|
|
527
|
+
dsn: str | None = None,
|
|
528
|
+
output_dir: str | None = None,
|
|
529
|
+
progress: bool = False,
|
|
530
|
+
profile: bool = False,
|
|
531
|
+
profile_limit: int = 20,
|
|
532
|
+
ping: bool = False,
|
|
533
|
+
lint: bool = False,
|
|
534
|
+
debug: bool = False,
|
|
535
|
+
config_file: str | None = None,
|
|
536
|
+
) -> None:
|
|
537
|
+
"""Initialise state, connect to the database, load the script, and run it.
|
|
538
|
+
|
|
539
|
+
Separated from argument parsing so it can be called directly in tests
|
|
540
|
+
without going through the Typer CLI layer. All parameters mirror the
|
|
541
|
+
corresponding CLI options; see [Syntax & Options](../syntax.md) for
|
|
542
|
+
descriptions.
|
|
543
|
+
|
|
544
|
+
When *ping* is ``True``, the function connects to the database, prints
|
|
545
|
+
connection details (DBMS name, server version, and location), and calls
|
|
546
|
+
:func:`_ping_db` which raises ``SystemExit(0)``. No script is loaded or
|
|
547
|
+
executed. *script_name* and *command* may both be ``None`` in ping mode.
|
|
548
|
+
|
|
549
|
+
When *lint* is ``True``, the script is parsed and statically analysed for
|
|
550
|
+
structural issues (unmatched IF/ENDIF/LOOP/BATCH blocks, potentially
|
|
551
|
+
undefined variables, missing INCLUDE files, empty scripts) without
|
|
552
|
+
connecting to a database or executing anything. Exits with code 0 if no
|
|
553
|
+
errors were found, or code 1 if errors were found.
|
|
554
|
+
"""
|
|
555
|
+
import execsql.state as _state
|
|
556
|
+
|
|
496
557
|
# ------------------------------------------------------------------
|
|
497
|
-
#
|
|
558
|
+
# Early setup: substitution variables seeded before config loading
|
|
498
559
|
# ------------------------------------------------------------------
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
560
|
+
_state.subvars = _seed_early_subvars()
|
|
561
|
+
|
|
562
|
+
# ------------------------------------------------------------------
|
|
563
|
+
# Read configuration file
|
|
564
|
+
# ------------------------------------------------------------------
|
|
565
|
+
_state.conf = _load_config(script_name, _state.subvars, config_file)
|
|
566
|
+
conf = _state.conf
|
|
567
|
+
|
|
568
|
+
# ------------------------------------------------------------------
|
|
569
|
+
# Connection string (--dsn / --connection-string): overrides -t/-u/-p
|
|
570
|
+
# and positional server/db args when provided.
|
|
571
|
+
# ------------------------------------------------------------------
|
|
572
|
+
if dsn:
|
|
573
|
+
db_type, dsn_user, dsn_port = _apply_dsn(dsn, conf, db_type)
|
|
574
|
+
if dsn_user:
|
|
575
|
+
user = dsn_user
|
|
576
|
+
if dsn_port:
|
|
577
|
+
port = dsn_port
|
|
578
|
+
|
|
579
|
+
# ------------------------------------------------------------------
|
|
580
|
+
# Merge CLI options over config-file values
|
|
581
|
+
# ------------------------------------------------------------------
|
|
582
|
+
_apply_cli_options(
|
|
583
|
+
conf,
|
|
584
|
+
user=user,
|
|
585
|
+
no_passwd=no_passwd,
|
|
586
|
+
database_encoding=database_encoding,
|
|
587
|
+
script_encoding=script_encoding,
|
|
588
|
+
output_encoding=output_encoding,
|
|
589
|
+
import_encoding=import_encoding,
|
|
590
|
+
import_buffer=import_buffer,
|
|
591
|
+
make_dirs=make_dirs,
|
|
592
|
+
boolean_int=boolean_int,
|
|
593
|
+
scanlines=scanlines,
|
|
594
|
+
use_gui=use_gui,
|
|
595
|
+
gui_framework=gui_framework,
|
|
596
|
+
db_type=db_type,
|
|
597
|
+
user_logfile=user_logfile,
|
|
598
|
+
port=port,
|
|
599
|
+
new_db=new_db,
|
|
600
|
+
output_dir=output_dir,
|
|
601
|
+
progress=progress,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# ------------------------------------------------------------------
|
|
605
|
+
# Positional arguments → server/db/db_file
|
|
606
|
+
# ------------------------------------------------------------------
|
|
607
|
+
_route_positionals(positional, conf, command=command, ping=ping)
|
|
608
|
+
|
|
609
|
+
# ------------------------------------------------------------------
|
|
610
|
+
# Script substitution variables that depend on the script path
|
|
611
|
+
# ------------------------------------------------------------------
|
|
612
|
+
_seed_script_subvars(_state.subvars, script_name)
|
|
613
|
+
|
|
614
|
+
# ------------------------------------------------------------------
|
|
615
|
+
# Initialise state objects
|
|
616
|
+
# ------------------------------------------------------------------
|
|
617
|
+
from execsql.metacommands import DISPATCH_TABLE
|
|
618
|
+
from execsql.metacommands.conditions import CONDITIONAL_TABLE
|
|
619
|
+
|
|
620
|
+
_state.initialize(conf, DISPATCH_TABLE, CONDITIONAL_TABLE)
|
|
621
|
+
|
|
622
|
+
# Local-only objects that require CLI-specific args or class definitions
|
|
623
|
+
_state.status = StatObj()
|
|
624
|
+
|
|
625
|
+
from execsql.config import WriteHooks
|
|
626
|
+
|
|
627
|
+
_state.output = WriteHooks()
|
|
628
|
+
|
|
629
|
+
import execsql.utils.fileio as _fileio
|
|
630
|
+
|
|
631
|
+
if _state.filewriter is None or not _state.filewriter.is_alive():
|
|
632
|
+
_fileio.filewriter = _state.filewriter = FileWriter(
|
|
633
|
+
_fileio.fw_input,
|
|
634
|
+
_fileio.fw_output,
|
|
635
|
+
file_encoding=conf.output_encoding,
|
|
636
|
+
open_timeout=getattr(conf, "outfile_open_timeout", 10),
|
|
637
|
+
)
|
|
638
|
+
_state.filewriter.start()
|
|
639
|
+
atexit.register(filewriter_end)
|
|
640
|
+
|
|
641
|
+
# ------------------------------------------------------------------
|
|
642
|
+
# Logging
|
|
643
|
+
# ------------------------------------------------------------------
|
|
644
|
+
_state.exec_log = _setup_logging(
|
|
645
|
+
conf,
|
|
646
|
+
_state.subvars,
|
|
647
|
+
script_name,
|
|
648
|
+
sub_vars,
|
|
649
|
+
boolean_int=boolean_int,
|
|
650
|
+
make_dirs=make_dirs,
|
|
651
|
+
database_encoding=database_encoding,
|
|
652
|
+
script_encoding=script_encoding,
|
|
653
|
+
output_encoding=output_encoding,
|
|
654
|
+
import_encoding=import_encoding,
|
|
655
|
+
user_logfile=user_logfile,
|
|
656
|
+
new_db=new_db,
|
|
657
|
+
port=port,
|
|
658
|
+
scanlines=scanlines,
|
|
659
|
+
db_type=db_type,
|
|
660
|
+
user=user,
|
|
661
|
+
use_gui=use_gui,
|
|
662
|
+
no_passwd=no_passwd,
|
|
663
|
+
import_buffer=import_buffer,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# ------------------------------------------------------------------
|
|
667
|
+
# Load the SQL script (skipped in --ping mode)
|
|
668
|
+
# ------------------------------------------------------------------
|
|
669
|
+
_ast_tree = None if ping else _load_script(command, script_name, conf.script_encoding)
|
|
510
670
|
|
|
511
671
|
# ------------------------------------------------------------------
|
|
512
672
|
# Dry-run: print command list and exit without connecting to DB
|