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.
- execsql/__main__.py +4 -5
- execsql/api.py +6 -0
- execsql/cli/__init__.py +3 -14
- execsql/cli/application.py +6 -4
- execsql/cli/commands/lint.py +5 -2
- execsql/cli/commands/lsp.py +42 -0
- execsql/cli/dsn.py +38 -2
- execsql/cli/lint.py +24 -7
- execsql/cli/run.py +22 -9
- execsql/config.py +58 -10
- execsql/data/execsql.conf.template +8 -2
- execsql/db/access.py +7 -5
- execsql/db/base.py +18 -0
- execsql/db/duckdb.py +96 -14
- execsql/db/factory.py +11 -1
- execsql/db/mysql.py +56 -3
- execsql/db/odbc.py +45 -0
- execsql/db/postgres.py +1 -1
- execsql/db/sqlserver.py +37 -21
- execsql/exporters/ods.py +146 -41
- execsql/exporters/values.py +166 -9
- execsql/gui/__init__.py +3 -31
- execsql/importers/base.py +1 -1
- execsql/importers/csv.py +1 -1
- execsql/importers/ods.py +2 -0
- execsql/lsp/_util.py +21 -0
- execsql/lsp/cache.py +73 -0
- execsql/lsp/code_actions.py +10 -14
- execsql/lsp/completion.py +8 -15
- execsql/lsp/diagnostics.py +15 -3
- execsql/lsp/document.py +142 -41
- execsql/lsp/hover.py +5 -13
- execsql/lsp/navigation.py +7 -10
- execsql/lsp/server.py +111 -29
- execsql/manifest.py +29 -9
- execsql/metacommands/__init__.py +0 -2
- execsql/metacommands/io_fileops.py +16 -5
- execsql/metacommands/reference.py +1 -1
- execsql/metacommands/upsert.py +10 -1
- execsql/script/executor.py +2 -21
- execsql/state.py +6 -3
- execsql/state.pyi +2 -0
- execsql/utils/auth.py +21 -14
- execsql/utils/errors.py +20 -37
- execsql/utils/fileio.py +44 -48
- execsql/utils/paths.py +31 -0
- execsql/utils/regex.py +0 -5
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/README.md +9 -8
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/md_compare.sql +53 -29
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/md_glossary.sql +20 -20
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/md_upsert.sql +59 -66
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/pg_compare.sql +19 -22
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/pg_glossary.sql +13 -17
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/script_template.sql +302 -299
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/ss_compare.sql +39 -29
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/ss_glossary.sql +13 -17
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/ss_upsert.sql +56 -66
- {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/METADATA +17 -43
- {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/RECORD +66 -63
- execsql2-2.29.0.data/data/execsql2_extras/pg_upsert.sql +0 -2791
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.29.0.data → execsql2-2.31.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/WHEEL +0 -0
- {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.29.0.dist-info → execsql2-2.31.1.dist-info}/licenses/LICENSE.txt +0 -0
- {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/
|
|
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
|
-
|
|
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,
|
|
95
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
62
|
+
Complete documentation is in the [Glossary Scripts](https://execsql2.readthedocs.io/en/stable/templates/glossary/) page of the execsql2 documentation.
|
|
61
63
|
|
|
62
|
-
##
|
|
64
|
+
## Tests
|
|
63
65
|
|
|
64
|
-
|
|
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
|
-
--
|
|
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.
|
|
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(
|
|
134
|
+
-- !x! if(not is_null("!!#include_cols!!"))
|
|
129
135
|
-- !x! sub ~col_sel and s.column_name in (!!#include_cols!!)
|
|
130
|
-
-- !x! elseif(
|
|
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
|
|
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(
|
|
275
|
+
-- !x! if(not is_null("!!#include_cols!!"))
|
|
266
276
|
-- !x! sub ~col_sel and s.column_name in (!!#include_cols!!)
|
|
267
|
-
-- !x! elseif(
|
|
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
|
|
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(
|
|
420
|
+
-- !x! if(not is_null("!!#include_cols!!"))
|
|
407
421
|
-- !x! sub ~col_sel and s.column_name in (!!#include_cols!!)
|
|
408
|
-
-- !x! elseif(
|
|
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
|
|
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(
|
|
574
|
+
-- !x! if(not is_null("!!#include_cols!!"))
|
|
557
575
|
-- !x! sub ~col_sel and s.column_name in (!!#include_cols!!)
|
|
558
|
-
-- !x! elseif(
|
|
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
|
|
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.
|