execsql2 2.29.0__py3-none-any.whl → 2.31.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 (67) hide show
  1. execsql/__main__.py +4 -5
  2. execsql/api.py +6 -0
  3. execsql/cli/__init__.py +3 -14
  4. execsql/cli/application.py +6 -4
  5. execsql/cli/commands/lint.py +5 -2
  6. execsql/cli/commands/lsp.py +42 -0
  7. execsql/cli/dsn.py +38 -2
  8. execsql/cli/lint.py +24 -7
  9. execsql/cli/run.py +22 -9
  10. execsql/config.py +58 -10
  11. execsql/data/execsql.conf.template +8 -2
  12. execsql/db/access.py +7 -5
  13. execsql/db/base.py +18 -0
  14. execsql/db/duckdb.py +96 -14
  15. execsql/db/factory.py +11 -1
  16. execsql/db/mysql.py +56 -3
  17. execsql/db/odbc.py +45 -0
  18. execsql/db/postgres.py +1 -1
  19. execsql/db/sqlserver.py +37 -21
  20. execsql/exporters/ods.py +146 -41
  21. execsql/exporters/values.py +166 -9
  22. execsql/gui/__init__.py +3 -31
  23. execsql/importers/base.py +1 -1
  24. execsql/importers/csv.py +1 -1
  25. execsql/importers/ods.py +2 -0
  26. execsql/lsp/_util.py +21 -0
  27. execsql/lsp/cache.py +73 -0
  28. execsql/lsp/code_actions.py +10 -14
  29. execsql/lsp/completion.py +8 -15
  30. execsql/lsp/diagnostics.py +15 -3
  31. execsql/lsp/document.py +142 -41
  32. execsql/lsp/hover.py +5 -13
  33. execsql/lsp/navigation.py +7 -10
  34. execsql/lsp/server.py +111 -29
  35. execsql/manifest.py +29 -9
  36. execsql/metacommands/__init__.py +0 -2
  37. execsql/metacommands/io_fileops.py +16 -5
  38. execsql/metacommands/reference.py +1 -1
  39. execsql/metacommands/upsert.py +10 -1
  40. execsql/script/executor.py +2 -21
  41. execsql/state.py +6 -3
  42. execsql/state.pyi +2 -0
  43. execsql/utils/auth.py +21 -14
  44. execsql/utils/errors.py +20 -37
  45. execsql/utils/fileio.py +44 -48
  46. execsql/utils/paths.py +31 -0
  47. execsql/utils/regex.py +0 -5
  48. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/README.md +9 -8
  49. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/md_compare.sql +53 -29
  50. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/md_glossary.sql +20 -20
  51. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/md_upsert.sql +59 -66
  52. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/pg_compare.sql +19 -22
  53. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/pg_glossary.sql +13 -17
  54. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/script_template.sql +302 -299
  55. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/ss_compare.sql +39 -29
  56. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/ss_glossary.sql +13 -17
  57. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/ss_upsert.sql +56 -66
  58. {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/METADATA +17 -43
  59. {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/RECORD +66 -63
  60. execsql2-2.29.0.data/data/execsql2_extras/pg_upsert.sql +0 -2791
  61. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  62. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  63. {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  64. {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/WHEEL +0 -0
  65. {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/entry_points.txt +0 -0
  66. {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/licenses/LICENSE.txt +0 -0
  67. {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/licenses/NOTICE +0 -0
execsql/utils/errors.py CHANGED
@@ -6,8 +6,6 @@ Error handling and reporting utilities for execsql.
6
6
  Provides the functions that form the error-reporting pipeline used
7
7
  throughout the codebase:
8
8
 
9
- - :func:`exception_info` — extracts type, value, and source location
10
- from the current exception.
11
9
  - :func:`exception_desc` — ``Type: message`` for the exception being
12
10
  handled, as shown on screen.
13
11
  - :func:`error_origin` / :func:`logged_error` — where in execsql an error
@@ -28,7 +26,6 @@ import execsql.state as _state
28
26
  from execsql.exceptions import ErrInfo
29
27
 
30
28
  __all__ = [
31
- "exception_info",
32
29
  "error_origin",
33
30
  "exception_desc",
34
31
  "logged_error",
@@ -40,7 +37,7 @@ __all__ = [
40
37
  "install_hint",
41
38
  ]
42
39
 
43
- _ADDING_AN_EXTRA = "https://execsql2.readthedocs.io/en/latest/getting-started/installation/#adding-an-extra-later"
40
+ _ADDING_AN_EXTRA = "https://execsql2.readthedocs.io/en/stable/getting-started/installation/#adding-an-extra-later"
44
41
 
45
42
 
46
43
  def install_hint(extra: str) -> str:
@@ -56,43 +53,29 @@ def install_hint(extra: str) -> str:
56
53
  )
57
54
 
58
55
 
59
- def exception_info() -> tuple:
60
- # Returns the exception type, value, source file name, source line number, and source line text.
61
- strace = traceback.extract_tb(sys.exc_info()[2])[-1:]
62
- traces = traceback.extract_tb(sys.exc_info()[2])
63
- xline = 0
64
- for trace in traces:
65
- if "execsql" in trace[0]:
66
- xline = trace[1]
67
- exc_message = ""
68
- exc_param = sys.exc_info()[1]
69
- if isinstance(exc_param, str):
70
- exc_message = exc_param
71
- elif exc_param is not None:
72
- message = getattr(exc_param, "message", None)
73
- value = getattr(exc_param, "value", None)
74
- if isinstance(message, str) and len(message) > 0:
75
- exc_message = message
76
- elif isinstance(value, str) and len(value) > 0:
77
- exc_message = value
78
- else:
79
- exc_message = str(exc_param)
80
- try:
81
- exc_message = str(exc_message)
82
- except Exception:
83
- exc_message = repr(exc_message)
84
- xinfo = sys.exc_info()[0]
85
- xname = getattr(xinfo, "__name__", "")
86
- return xname, exc_message, strace[0][0], xline, strace[0][3]
87
-
88
-
89
56
  def exception_desc() -> str:
90
57
  """``Type: message`` for the exception being handled, as shown to the person running the script.
91
58
 
92
- Where in execsql it was raised is for the log: see :func:`error_origin`.
59
+ The message is the exception's ``message`` or ``value`` attribute when it
60
+ has a non-empty string one (as some drivers' exceptions do), else its
61
+ ``str()``. Where in execsql it was raised is for the log: see
62
+ :func:`error_origin`.
93
63
  """
94
- exc_type, exc_strval, _filename, _lineno, _linetext = exception_info()
95
- return f"{exc_type}: {exc_strval}"
64
+ exc_type, exc = sys.exc_info()[:2]
65
+ if isinstance(exc, str):
66
+ message = exc
67
+ elif exc is None:
68
+ message = ""
69
+ else:
70
+ attr = next(
71
+ (v for v in (getattr(exc, "message", None), getattr(exc, "value", None)) if isinstance(v, str) and v),
72
+ None,
73
+ )
74
+ try:
75
+ message = attr if attr is not None else str(exc)
76
+ except Exception:
77
+ message = repr(exc)
78
+ return f"{getattr(exc_type, '__name__', '')}: {message}"
96
79
 
97
80
 
98
81
  def error_origin(exc: BaseException) -> str | None:
execsql/utils/fileio.py CHANGED
@@ -34,7 +34,6 @@ import threading
34
34
  import time
35
35
  import weakref
36
36
  from collections.abc import Callable
37
- from encodings.aliases import aliases as codec_dict
38
37
  from typing import Any, NamedTuple, cast
39
38
 
40
39
  from execsql.exceptions import ErrInfo
@@ -47,14 +46,11 @@ __all__ = [
47
46
  "EncodedFile",
48
47
  "Logger",
49
48
  "TempFileMgr",
50
- "list_encodings",
51
49
  "filewriter_filestatus",
52
50
  "filewriter_write",
53
51
  "filewriter_open_as_new",
54
52
  "filewriter_close",
55
53
  "filewriter_close_all_after_write",
56
- "filewriter_closeall",
57
- "filewriter_shutdown",
58
54
  "filewriter_end",
59
55
  "redact",
60
56
  "register_secret",
@@ -216,9 +212,8 @@ class FileWriter(threading.Thread):
216
212
  CMD_OPEN_AS_NEW,
217
213
  CMD_CLOSE_ALL_AFTER_WRITE,
218
214
  CMD_CLOSED_STATUS,
219
- CMD_PING,
220
215
  CMD_OPEN_FAILURES,
221
- ) = range(10)
216
+ ) = range(9)
222
217
 
223
218
  class FileControl:
224
219
  STATUS_OPEN, STATUS_WAITING, STATUS_UNOPENED, STATUS_CLOSED, STATUS_OPENFAILURE, STATUS_WRITEFAILURE = range(6)
@@ -375,7 +370,6 @@ class FileWriter(threading.Thread):
375
370
  self.open_as_new,
376
371
  self.close_all_after_write,
377
372
  self.closed_status,
378
- self.ping,
379
373
  self.open_failures,
380
374
  )
381
375
 
@@ -417,10 +411,6 @@ class FileWriter(threading.Thread):
417
411
  if fc.status != self.FileControl.STATUS_CLOSED and fc.status not in fc.FAILED:
418
412
  self._on_file(fc, fc.clean_close)
419
413
 
420
- def ping(self, token: object) -> None:
421
- """Echo *token* back on the return queue — used to synchronize callers."""
422
- self.return_msg_queue.put(token)
423
-
424
414
  def retry_waiting(self) -> None:
425
415
  """Re-attempt opening every file still waiting on a lock.
426
416
 
@@ -526,7 +516,7 @@ class FileWriter(threading.Thread):
526
516
  the rest, so one bad command cannot stop the thread (and with it
527
517
  every later write, to any file).
528
518
  """
529
- filename = argtuple[0] if command != self.CMD_PING and argtuple and isinstance(argtuple[0], str) else ""
519
+ filename = argtuple[0] if argtuple and isinstance(argtuple[0], str) else ""
530
520
  lost = LostOutput(filename, 0, str(exc), "writer")
531
521
  if command == self.CMD_OPEN_FAILURES:
532
522
  self.return_msg_queue.put([lost])
@@ -535,7 +525,6 @@ class FileWriter(threading.Thread):
535
525
  replies: dict[int, Any] = {
536
526
  self.CMD_GET_STATUS: self.FileControl.STATUS_CLOSED,
537
527
  self.CMD_CLOSED_STATUS: self.FileControl.STATUS_CLOSED,
538
- self.CMD_PING: argtuple[0] if argtuple else None,
539
528
  }
540
529
  if command in replies:
541
530
  self.return_msg_queue.put(replies[command])
@@ -745,20 +734,6 @@ def _lost_output_message(lost: LostOutput, open_timeout: int) -> str:
745
734
  return f"{cause}; {lost.dropped} line(s) of output to it were discarded" if lost.dropped else cause
746
735
 
747
736
 
748
- def filewriter_closeall() -> None:
749
- fw = _active_writer()
750
- if fw is None:
751
- return
752
- fw.input_queue.put((FileWriter.CMD_CLOSE_ALL, ()))
753
-
754
-
755
- def filewriter_shutdown() -> None:
756
- fw = _active_writer()
757
- if fw is None:
758
- return
759
- fw.input_queue.put((FileWriter.CMD_SHUTDOWN, ()))
760
-
761
-
762
737
  def filewriter_end(writer: FileWriter | None = None) -> None:
763
738
  """Close every file and stop *writer*, or the active run's writer if omitted.
764
739
 
@@ -894,6 +869,8 @@ class Logger:
894
869
  self.server_name = server_name
895
870
  self.cmdline_options = cmdline_options
896
871
  self._redaction_values: list[str] = []
872
+ # Of those, the -a / --var values: masked in the log, not in the manifest.
873
+ self._argument_values: set[str] = set()
897
874
  if log_file_name:
898
875
  self.log_file_name = log_file_name
899
876
  else:
@@ -956,9 +933,9 @@ class Logger:
956
933
  )
957
934
  self.writelog(self._redact(msg) or "")
958
935
  if server_name:
959
- msg = f"run_db_server\t{self.run_id}\t{server_name}\t{db_name}\n"
936
+ msg = f"run_db_server\t{self.run_id}\t{self._redact(server_name)}\t{self._redact(db_name)}\n"
960
937
  else:
961
- msg = f"run_db_file\t{self.run_id}\t{db_name}\n"
938
+ msg = f"run_db_file\t{self.run_id}\t{self._redact(db_name)}\n"
962
939
  self.writelog(msg)
963
940
  self.seq_no = 0
964
941
  atexit.register(self.close)
@@ -974,20 +951,41 @@ class Logger:
974
951
 
975
952
  return _datetime.datetime.now().isoformat(timespec="seconds")
976
953
 
977
- def add_redaction_value(self, value: str | None) -> None:
954
+ def add_redaction_value(self, value: str | None, *, argument: bool = False) -> None:
955
+ """Mask *value* in every later log record.
956
+
957
+ *argument* marks an ``-a`` / ``--var`` value: it may be a secret, so
958
+ the log masks it, but it is just as often part of a path, so the
959
+ ``--manifest`` JSON (``redact(..., arguments=False)``) does not. A
960
+ value registered both ways is a secret.
961
+ """
978
962
  if value is None:
979
963
  return
980
964
  value = str(value)
981
- if not value or value in self._redaction_values:
965
+ if not value:
966
+ return
967
+ if not argument:
968
+ self._argument_values.discard(value)
969
+ if value in self._redaction_values:
982
970
  return
983
971
  self._redaction_values.append(value)
972
+ if argument:
973
+ self._argument_values.add(value)
974
+
975
+ def _redact(self, msg: str | None, *, arguments: bool = True) -> str | None:
976
+ """*msg* with registered secrets and common secret shapes replaced by ``***``.
984
977
 
985
- def _redact(self, msg: str | None) -> str | None:
978
+ Every record applies it to its free-text fields (messages, names,
979
+ paths), never to a whole line: a registered value can be as short as
980
+ a ``-a 2024`` argument, which would otherwise mask timestamps and
981
+ sequence numbers.
982
+ """
986
983
  if msg is None:
987
984
  return None
988
985
  redacted = str(msg)
989
986
  for value in sorted(self._redaction_values, key=len, reverse=True):
990
- redacted = redacted.replace(value, "***")
987
+ if arguments or value not in self._argument_values:
988
+ redacted = redacted.replace(value, "***")
991
989
  for pattern in self._COMMON_SECRET_PATTERNS:
992
990
  if pattern.pattern.startswith("(?i)(://"):
993
991
  redacted = pattern.sub(r"\1***\3", redacted)
@@ -1026,12 +1024,13 @@ class Logger:
1026
1024
 
1027
1025
  def log_db_connect(self, db: Any) -> None:
1028
1026
  self.seq_no += 1
1029
- msg = f"connect\t{self.run_id}\t{self.seq_no}\t{self._ts()}\t{db.name()}\n"
1027
+ msg = f"connect\t{self.run_id}\t{self.seq_no}\t{self._ts()}\t{self._redact(db.name())}\n"
1030
1028
  self.writelog(msg)
1031
1029
 
1032
1030
  def log_action_export(self, line_no: int, query_name: str, export_file_name: str) -> None:
1033
1031
  self.seq_no += 1
1034
- msg = f"action\t{self.run_id}\t{self.seq_no}\t{self._ts()}\texport\t{line_no}\tQuery {query_name} exported to {export_file_name}\n"
1032
+ description = self._redact(f"Query {query_name} exported to {export_file_name}")
1033
+ msg = f"action\t{self.run_id}\t{self.seq_no}\t{self._ts()}\texport\t{line_no}\t{description}\n"
1035
1034
  self.writelog(msg)
1036
1035
 
1037
1036
  def log_status_exception(self, msg: str | None) -> None:
@@ -1087,7 +1086,7 @@ class Logger:
1087
1086
  self.exit_type = "halt"
1088
1087
  self.exit_scriptfile = script_file_name
1089
1088
  self.exit_lno = line_no
1090
- self.exit_description = msg
1089
+ self.exit_description = None if not msg else msg.replace("\n", "")
1091
1090
 
1092
1091
  def log_exit_interrupted(self) -> None:
1093
1092
  # Save values to be used by exit() function triggered on program exit
@@ -1121,9 +1120,9 @@ class Logger:
1121
1120
  wmsg = "exit\t{}\t{}\t{}({})\t{}\t{:.1f}s\n".format(
1122
1121
  self.run_id,
1123
1122
  self.exit_type,
1124
- self.exit_scriptfile or "",
1123
+ self._redact(self.exit_scriptfile) or "",
1125
1124
  str(self.exit_lno or ""),
1126
- self.exit_description or "",
1125
+ self._redact(self.exit_description) or "", # a HALT MESSAGE can hold any variable's value
1127
1126
  elapsed,
1128
1127
  )
1129
1128
  self.writelog(wmsg)
@@ -1144,12 +1143,16 @@ def register_secret(value: str | None) -> None:
1144
1143
  add(value)
1145
1144
 
1146
1145
 
1147
- def redact(text: str) -> str:
1148
- """*text* with the active run's registered secrets and common secret shapes replaced by ``***``."""
1146
+ def redact(text: str, *, arguments: bool = True) -> str:
1147
+ """*text* with the active run's registered secrets and common secret shapes replaced by ``***``.
1148
+
1149
+ With *arguments* false, ``-a`` / ``--var`` values are left as they are
1150
+ (see :meth:`Logger.add_redaction_value`).
1151
+ """
1149
1152
  import execsql.state as _state
1150
1153
 
1151
1154
  redactor = getattr(_state.exec_log, "_redact", None)
1152
- return redactor(text) if redactor is not None else text
1155
+ return (redactor(text, arguments=arguments) or "") if redactor is not None else text
1153
1156
 
1154
1157
 
1155
1158
  class TempFileMgr:
@@ -1188,10 +1191,3 @@ _live_tempfile_mgrs: weakref.WeakSet[TempFileMgr] = weakref.WeakSet()
1188
1191
  def _remove_all_temp_files() -> None:
1189
1192
  for mgr in list(_live_tempfile_mgrs):
1190
1193
  mgr.remove_all()
1191
-
1192
-
1193
- def list_encodings() -> None:
1194
- enc = list(codec_dict.keys())
1195
- enc.sort()
1196
- msg = f"Encodings: {', '.join(enc)}\n"
1197
- print(msg)
execsql/utils/paths.py ADDED
@@ -0,0 +1,31 @@
1
+ """Telling a path on this machine from one that names another.
2
+
3
+ On Windows, checking or opening a path written as ``\\\\host\\share\\...``
4
+ (or ``//host/share/...``) makes the OS contact *host* and offer the logged-in
5
+ user's credentials. The static tools (``execsql lint``, ``execsql format``
6
+ and the language server) read paths written in scripts and config files
7
+ someone else may have written, so they never touch such a path.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+
14
+ __all__ = ["is_network_path"]
15
+
16
+ # Read once; tests set it to check the Windows rules on any OS.
17
+ _WINDOWS = os.name == "nt"
18
+
19
+
20
+ def is_network_path(path: str | os.PathLike[str]) -> bool:
21
+ """Whether *path*, as written, names another machine or a device.
22
+
23
+ On Windows: a UNC path (``\\\\host\\share``, ``//host/share``, either slash)
24
+ or a ``\\\\?\\`` / ``\\\\.\\`` device path. Elsewhere a leading double
25
+ slash is an ordinary local path, so nothing is. A relative path is never
26
+ one: it stays on the machine of the file that names it.
27
+ """
28
+ if not _WINDOWS:
29
+ return False
30
+ text = os.fspath(path)
31
+ return len(text) >= 2 and text[0] in "\\/" and text[1] in "\\/"
execsql/utils/regex.py CHANGED
@@ -16,7 +16,6 @@ import os
16
16
 
17
17
  __all__ = [
18
18
  "ins_rxs",
19
- "ins_quoted_rx",
20
19
  "ins_schema_rxs",
21
20
  "ins_table_rxs",
22
21
  "ins_table_list_rxs",
@@ -42,10 +41,6 @@ def ins_rxs(rx_list: tuple[str, ...], fragment1: object, fragment2: object) -> t
42
41
  return tuple(rv)
43
42
 
44
43
 
45
- def ins_quoted_rx(fragment1: object, fragment2: object, rx: str) -> tuple[str, ...]:
46
- return ins_rxs((rx, rf'"{rx}"'), fragment1, fragment2)
47
-
48
-
49
44
  def ins_schema_rxs(fragment1: object, fragment2: object, suffix: str | None = None) -> tuple[str, ...]:
50
45
  schema_exprs: tuple[str, ...] = (
51
46
  r'"(?P<schema>[A-Za-z0-9_\- ]+)"',
@@ -1,12 +1,14 @@
1
1
  # Templates for execsql
2
2
 
3
+ The [Templates section](https://execsql2.readthedocs.io/en/stable/templates/) of the execsql2 documentation describes how to use these files.
4
+
3
5
  Several types of templates are provided that may be useful in conjunction with execsql. These are:
4
6
 
5
7
  - **execsql.conf** — An annotated reference of every configuration setting with notes on its usage. Generate a fresh copy in any directory with `execsql config --init > execsql.conf`; the canonical content ships inside the installed package and is loaded via `importlib.resources` (no need to find or copy a source file).
6
8
 
7
9
  - **script_template.sql** — A framework for SQL scripts that make use of several execsql features. It includes sections for custom configuration settings, custom logfile creation, and reporting of unexpected script exits (through user cancellation or errors).
8
10
 
9
- - **config_settings.sqlite** and **example_config_prompt.sql** — A SQLite database containing specifications for all settings configurable with the CONFIG metacommand, in the form used by the PROMPT ENTRY_FORM metacommand, and a SQL script illustrating how this database can be used to prompt the user for some or all of the configuration settings. *execsql* version 1.63.0 or later is needed to use this script.
11
+ - **config_settings.sqlite** and **example_config_prompt.sql** — A SQLite database containing specifications for all settings configurable with the CONFIG metacommand, in the form used by the PROMPT ENTRY_FORM metacommand, and a SQL script illustrating how this database can be used to prompt the user for some or all of the configuration settings.
10
12
 
11
13
  ## Functional Scripts for execsql
12
14
 
@@ -16,7 +18,7 @@ These script files provide useful functionality that should be accessed by inclu
16
18
 
17
19
  These scripts perform automated upsert operations on any tables of any database in a DBMS that supports the standard *information_schema* views. Currently that includes:
18
20
 
19
- - **PostgreSQL:** `pg_upsert.sql`
21
+ - **PostgreSQL:** use the [`PG_UPSERT`](https://execsql2.readthedocs.io/en/stable/reference/metacommands/#pg_upsert) metacommand, which does the same with the [pg-upsert](https://pg-upsert.readthedocs.io/) package; it replaced `pg_upsert.sql`.
20
22
  - **MySQL/MariaDB:** `md_upsert.sql`
21
23
  - **SQL Server:** `ss_upsert.sql`
22
24
 
@@ -35,7 +37,7 @@ Features of these upsert scripts include:
35
37
  - Optionally, they will record all operations carried out in a custom log file; this log may include the SQL statements executed and the data values that were added or changed.
36
38
  - If an execsql console is active, they will use the console's status bar and progress bar to indicate the activity underway.
37
39
 
38
- Complete documentation is available at [Read The Docs (execsql-upsert)](https://execsql-upsert.readthedocs.io/en/latest/).
40
+ Complete documentation is in the [Upsert Scripts](https://execsql2.readthedocs.io/en/stable/templates/upsert/) page of the execsql2 documentation.
39
41
 
40
42
  ### Table Comparison Scripts
41
43
 
@@ -47,7 +49,7 @@ These scripts work with any tables of any database in a DBMS that supports the s
47
49
  - **MySQL/MariaDB:** `md_compare.sql`
48
50
  - **SQL Server:** `ss_compare.sql`
49
51
 
50
- Complete documentation is available at [Read The Docs (execsql-compare)](https://execsql-compare.readthedocs.io/en/latest/).
52
+ Complete documentation is in the [Compare Scripts](https://execsql2.readthedocs.io/en/stable/templates/compare/) page of the execsql2 documentation.
51
53
 
52
54
  ### Glossary Creation Scripts
53
55
 
@@ -57,9 +59,8 @@ These scripts create a glossary of column names or other terms that can be expor
57
59
  - **MySQL/MariaDB:** `md_glossary.sql`
58
60
  - **SQL Server:** `ss_glossary.sql`
59
61
 
60
- Complete documentation is available at [Read The Docs (execsql-glossary)](https://execsql-compare.readthedocs.io/en/latest/).
62
+ Complete documentation is in the [Glossary Scripts](https://execsql2.readthedocs.io/en/stable/templates/glossary/) page of the execsql2 documentation.
61
63
 
62
- ## Roadmap
64
+ ## Tests
63
65
 
64
- - [ ] Integrate documentation for [execsql-compare](https://execsql-compare.readthedocs.io/en/latest/) into the main execsql2 documentation site.
65
- - [ ] Integrate documentation for [execsql-upsert](https://execsql-upsert.readthedocs.io/en/latest/) into the main execsql2 documentation site.
66
+ `tests/templates/` runs these scripts against live databases: the glossary, compare and upsert scripts on PostgreSQL, MySQL, MariaDB and SQL Server (`md_` scripts on both MySQL and MariaDB), plus lint and INCLUDE checks for every file. Run them with `just test-templates`; see `CONTRIBUTING.md`.
@@ -29,8 +29,9 @@
29
29
  -- 3. Temporary tables and views created by these scripts all
30
30
  -- begin with "cmp_".
31
31
  --
32
- -- AUTHOR
32
+ -- AUTHORS
33
33
  -- Dreas Nielsen (RDN)
34
+ -- Caleb Grant (CG)
34
35
  --
35
36
  -- COPYRIGHT AND LICENSE
36
37
  -- Copyright (c) 2020 R.Dreas Nielsen
@@ -45,7 +46,7 @@
45
46
  -- The GNU General Public License is available at <http://www.gnu.org/licenses/>
46
47
  --
47
48
  -- VERSION
48
- -- 1.1.1
49
+ -- 1.2.0
49
50
  --
50
51
  -- HISTORY
51
52
  -- Date Remarks
@@ -53,13 +54,20 @@
53
54
  -- 2020-02-27 Created from v. 1.1.0 of pg_compare.sql RDN.
54
55
  -- 2020-02-29 Excluded PK columns from output of COMPARE_COMMON_VALS.
55
56
  -- Completed conversion to md. v. 1.1.1. RDN.
57
+ -- 2026-10-15 execsql2: text values compare case- and accent-sensitively (the
58
+ -- default collation reported 'y' -> 'Y' as unchanged); columns
59
+ -- are read from the current database only (a same-named table in
60
+ -- another database was mixed in); each script's notes are its
61
+ -- docstring; include_cols and exclude_cols are declared optional
62
+ -- parameters. v. 1.2.0. Tested in tests/templates of the
63
+ -- execsql2 repository. CG.
56
64
  -- ==================================================================
57
65
 
58
66
 
59
67
  -- ################################################################
60
68
  -- Script COMPARE_COMMON
61
69
  -- ===============================================================
62
- --
70
+ -- !x! BEGIN SCRIPT COMPARE_COMMON with parameters (stage_pfx, table, sql_var, include_cols="", exclude_cols="")
63
71
  -- Generate SQL to compare the attributes of base and staging
64
72
  -- tables for only those primary keys in common. This uses an
65
73
  -- inner join between the tables, and does not include any rows
@@ -97,8 +105,6 @@
97
105
  --
98
106
  -- Notes
99
107
  -- 1. The generated SQL is not terminated with a semicolon.
100
- -- ===============================================================
101
- -- !x! BEGIN SCRIPT COMPARE_COMMON with parameters (stage_pfx, table, sql_var)
102
108
 
103
109
  -- Create a table of primary key columns for the table
104
110
  drop table if exists cmp_primary_key_columns cascade;
@@ -125,20 +131,26 @@ order by k.ordinal_position
125
131
  -- that are to be compared.
126
132
  -- Include only those columns from the staging table that are also in the base table.
127
133
  -- !x! sub_empty ~col_sel
128
- -- !x! if(sub_defined(#include_cols))
134
+ -- !x! if(not is_null("!!#include_cols!!"))
129
135
  -- !x! sub ~col_sel and s.column_name in (!!#include_cols!!)
130
- -- !x! elseif(sub_defined(#exclude_cols))
136
+ -- !x! elseif(not is_null("!!#exclude_cols!!"))
131
137
  -- !x! sub ~col_sel and s.column_name not in (!!#exclude_cols!!)
132
138
  -- !x! endif
133
139
  drop table if exists cmp_cols cascade;
134
140
  create table cmp_cols as
135
- select s.column_name
141
+ select
142
+ s.column_name,
143
+ -- Text compares case- and accent-sensitively below; the default collation does not.
144
+ (s.data_type in ('char', 'varchar', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum', 'set') and b.data_type in ('char', 'varchar', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum', 'set')) as is_text
136
145
  from information_schema.columns as s
137
146
  inner join information_schema.columns as b on s.column_name=b.column_name
138
147
  left join cmp_primary_key_columns as pk on pk.column_name = s.column_name
139
148
  where
140
149
  s.table_name = concat(!'!#stage_pfx!'!, !'!#table!'!)
141
150
  and b.table_name = !'!#table!'!
151
+ -- information_schema covers every database on the server.
152
+ and s.table_schema = database()
153
+ and b.table_schema = database()
142
154
  and pk.column_name is null
143
155
  !!~col_sel!!
144
156
  order by s.ordinal_position;
@@ -157,7 +169,7 @@ from
157
169
  alter table cmp_cols add column sqlcmd character varying(500);
158
170
  update cmp_cols
159
171
  set sqlcmd =
160
- concat('case when b.', column_name, ' is null then case when s.', column_name, ' is null then ''Same'' else ''New'' end else case when s.', column_name, ' is null then ''Old'' else case when b.', column_name, ' = s.', column_name, ' then ''Same'' else ''Different'' end end end as ', column_name)
172
+ concat('case when b.', column_name, ' is null then case when s.', column_name, ' is null then ''Same'' else ''New'' end else case when s.', column_name, ' is null then ''Old'' else case when ', if(is_text, concat('cast(b.', column_name, ' as binary) = cast(s.', column_name, ' as binary)'), concat('b.', column_name, ' = s.', column_name)), ' then ''Same'' else ''Different'' end end end as ', column_name)
161
173
  ;
162
174
 
163
175
  -- Create the comparison expression for all columns.
@@ -200,7 +212,7 @@ drop table if exists cmp_primary_key_columns cascade;
200
212
  -- ################################################################
201
213
  -- Script COMPARE_COMMON_VALS
202
214
  -- ===============================================================
203
- --
215
+ -- !x! BEGIN SCRIPT COMPARE_COMMON_VALS with parameters (stage_pfx, table, sql_var, include_cols="", exclude_cols="")
204
216
  -- Generate SQL to compare the attributes of base and staging
205
217
  -- tables for only those primary keys in common. This uses an
206
218
  -- inner join between the tables, and does not include any rows
@@ -234,8 +246,6 @@ drop table if exists cmp_primary_key_columns cascade;
234
246
  --
235
247
  -- Notes
236
248
  -- 1. The generated SQL is not terminated with a semicolon.
237
- -- ===============================================================
238
- -- !x! BEGIN SCRIPT COMPARE_COMMON_VALS with parameters (stage_pfx, table, sql_var)
239
249
 
240
250
  -- Create a table of primary key columns for the table
241
251
  drop table if exists cmp_primary_key_columns cascade;
@@ -262,20 +272,26 @@ order by k.ordinal_position
262
272
  -- that are to be compared.
263
273
  -- Include only those columns from the staging table that are also in the base table.
264
274
  -- !x! sub_empty ~col_sel
265
- -- !x! if(sub_defined(#include_cols))
275
+ -- !x! if(not is_null("!!#include_cols!!"))
266
276
  -- !x! sub ~col_sel and s.column_name in (!!#include_cols!!)
267
- -- !x! elseif(sub_defined(#exclude_cols))
277
+ -- !x! elseif(not is_null("!!#exclude_cols!!"))
268
278
  -- !x! sub ~col_sel and s.column_name not in (!!#exclude_cols!!)
269
279
  -- !x! endif
270
280
  drop table if exists cmp_cols cascade;
271
281
  create table cmp_cols as
272
- select s.column_name
282
+ select
283
+ s.column_name,
284
+ -- Text compares case- and accent-sensitively below; the default collation does not.
285
+ (s.data_type in ('char', 'varchar', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum', 'set') and b.data_type in ('char', 'varchar', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum', 'set')) as is_text
273
286
  from information_schema.columns as s
274
287
  inner join information_schema.columns as b on s.column_name=b.column_name
275
288
  left join cmp_primary_key_columns as pk on pk.column_name = s.column_name
276
289
  where
277
290
  s.table_name = concat(!'!#stage_pfx!'!, !'!#table!'!)
278
291
  and b.table_name = !'!#table!'!
292
+ -- information_schema covers every database on the server.
293
+ and s.table_schema = database()
294
+ and b.table_schema = database()
279
295
  and pk.column_name is null
280
296
  !!~col_sel!!
281
297
  order by s.ordinal_position;
@@ -341,7 +357,7 @@ drop table if exists cmp_primary_key_columns cascade;
341
357
  -- ################################################################
342
358
  -- Script COMPARE_CHANGES
343
359
  -- ===============================================================
344
- --
360
+ -- !x! BEGIN SCRIPT COMPARE_CHANGES with parameters (stage_pfx, table, sql_var, include_cols="", exclude_cols="")
345
361
  -- Generate SQL to characterize every row of the staging table
346
362
  -- data as either a new row, a row with changed data, or a row
347
363
  -- with data that are unchanged relative to the base table.
@@ -374,8 +390,6 @@ drop table if exists cmp_primary_key_columns cascade;
374
390
  --
375
391
  -- Notes
376
392
  -- 1. The generated SQL is not terminated with a semicolon.
377
- -- ===============================================================
378
- -- !x! BEGIN SCRIPT COMPARE_CHANGES with parameters (stage_pfx, table, sql_var)
379
393
 
380
394
  -- Create a table of primary key columns for the table
381
395
  drop table if exists cmp_primary_key_columns cascade;
@@ -403,20 +417,26 @@ order by k.ordinal_position
403
417
  -- that are to be compared.
404
418
  -- Include only those columns from the staging table that are also in the base table.
405
419
  -- !x! sub_empty ~col_sel
406
- -- !x! if(sub_defined(#include_cols))
420
+ -- !x! if(not is_null("!!#include_cols!!"))
407
421
  -- !x! sub ~col_sel and s.column_name in (!!#include_cols!!)
408
- -- !x! elseif(sub_defined(#exclude_cols))
422
+ -- !x! elseif(not is_null("!!#exclude_cols!!"))
409
423
  -- !x! sub ~col_sel and s.column_name not in (!!#exclude_cols!!)
410
424
  -- !x! endif
411
425
  drop table if exists cmp_cols cascade;
412
426
  create table cmp_cols as
413
- select s.column_name
427
+ select
428
+ s.column_name,
429
+ -- Text compares case- and accent-sensitively below; the default collation does not.
430
+ (s.data_type in ('char', 'varchar', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum', 'set') and b.data_type in ('char', 'varchar', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum', 'set')) as is_text
414
431
  from information_schema.columns as s
415
432
  inner join information_schema.columns as b on s.column_name=b.column_name
416
433
  left join cmp_primary_key_columns as pk on pk.column_name = s.column_name
417
434
  where
418
435
  s.table_name = concat(!'!#stage_pfx!'!, !'!#table!'!)
419
436
  and b.table_name = !'!#table!'!
437
+ -- information_schema covers every database on the server.
438
+ and s.table_schema = database()
439
+ and b.table_schema = database()
420
440
  and pk.column_name is null
421
441
  !!~col_sel!!
422
442
  order by s.ordinal_position;
@@ -436,7 +456,7 @@ from
436
456
  alter table cmp_cols add column sqlcmd character varying(500);
437
457
  update cmp_cols
438
458
  set sqlcmd =
439
- concat('case when b.', column_name, ' is null then case when s.', column_name, ' is null then 0 else 1 end else case when s.', column_name, ' is null then 1 else case when b.', column_name, ' = s.', column_name, ' then 0 else 1 end end end')
459
+ concat('case when b.', column_name, ' is null then case when s.', column_name, ' is null then 0 else 1 end else case when s.', column_name, ' is null then 1 else case when ', if(is_text, concat('cast(b.', column_name, ' as binary) = cast(s.', column_name, ' as binary)'), concat('b.', column_name, ' = s.', column_name)), ' then 0 else 1 end end end')
440
460
  ;
441
461
 
442
462
  -- Create the expression to determine whether there is a difference for any column.
@@ -491,7 +511,7 @@ drop table if exists cmp_primary_key_columns cascade;
491
511
  -- ################################################################
492
512
  -- Script COMPARE_CHANGED
493
513
  -- ===============================================================
494
- --
514
+ -- !x! BEGIN SCRIPT COMPARE_CHANGED with parameters (stage_pfx, table, sql_var, include_cols="", exclude_cols="")
495
515
  -- Generate SQL to characterize every row of the base table
496
516
  -- depending on whether there is a matching row in the staging
497
517
  -- table, whether a matching row has new data, or whether a
@@ -525,8 +545,6 @@ drop table if exists cmp_primary_key_columns cascade;
525
545
  --
526
546
  -- Notes
527
547
  -- 1. The generated SQL is not terminated with a semicolon.
528
- -- ===============================================================
529
- -- !x! BEGIN SCRIPT COMPARE_CHANGED with parameters (stage_pfx, table, sql_var)
530
548
 
531
549
  -- Create a table of primary key columns for the table
532
550
  drop table if exists cmp_primary_key_columns cascade;
@@ -553,20 +571,26 @@ order by k.ordinal_position
553
571
  -- that are to be compared.
554
572
  -- Include only those columns from the staging table that are also in the base table.
555
573
  -- !x! sub_empty ~col_sel
556
- -- !x! if(sub_defined(#include_cols))
574
+ -- !x! if(not is_null("!!#include_cols!!"))
557
575
  -- !x! sub ~col_sel and s.column_name in (!!#include_cols!!)
558
- -- !x! elseif(sub_defined(#exclude_cols))
576
+ -- !x! elseif(not is_null("!!#exclude_cols!!"))
559
577
  -- !x! sub ~col_sel and s.column_name not in (!!#exclude_cols!!)
560
578
  -- !x! endif
561
579
  drop table if exists cmp_cols cascade;
562
580
  create table cmp_cols as
563
- select s.column_name
581
+ select
582
+ s.column_name,
583
+ -- Text compares case- and accent-sensitively below; the default collation does not.
584
+ (s.data_type in ('char', 'varchar', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum', 'set') and b.data_type in ('char', 'varchar', 'tinytext', 'text', 'mediumtext', 'longtext', 'enum', 'set')) as is_text
564
585
  from information_schema.columns as s
565
586
  inner join information_schema.columns as b on s.column_name=b.column_name
566
587
  left join cmp_primary_key_columns as pk on pk.column_name = s.column_name
567
588
  where
568
589
  s.table_name = concat(!'!#stage_pfx!'!, !'!#table!'!)
569
590
  and b.table_name = !'!#table!'!
591
+ -- information_schema covers every database on the server.
592
+ and s.table_schema = database()
593
+ and b.table_schema = database()
570
594
  and pk.column_name is null
571
595
  !!~col_sel!!
572
596
  order by s.ordinal_position;
@@ -586,7 +610,7 @@ from
586
610
  alter table cmp_cols add column sqlcmd character varying(500);
587
611
  update cmp_cols
588
612
  set sqlcmd =
589
- concat('case when b.', column_name, ' is null then case when s.', column_name, ' is null then 0 else 1 end else case when s.', column_name, ' is null then 1 else case when b.', column_name, ' = s.', column_name, ' then 0 else 1 end end end')
613
+ concat('case when b.', column_name, ' is null then case when s.', column_name, ' is null then 0 else 1 end else case when s.', column_name, ' is null then 1 else case when ', if(is_text, concat('cast(b.', column_name, ' as binary) = cast(s.', column_name, ' as binary)'), concat('b.', column_name, ' = s.', column_name)), ' then 0 else 1 end end end')
590
614
  ;
591
615
 
592
616
  -- Create the expression to determine whether there is a difference for any column.