execsql2 2.30.0__py3-none-any.whl → 2.31.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.
Files changed (49) hide show
  1. execsql/api.py +1 -0
  2. execsql/cli/application.py +1 -1
  3. execsql/cli/commands/lint.py +1 -1
  4. execsql/cli/dsn.py +35 -1
  5. execsql/cli/lint.py +10 -1
  6. execsql/cli/run.py +8 -3
  7. execsql/config.py +26 -4
  8. execsql/data/execsql.conf.template +8 -2
  9. execsql/db/access.py +9 -5
  10. execsql/db/base.py +18 -0
  11. execsql/db/dsn.py +3 -0
  12. execsql/db/factory.py +11 -1
  13. execsql/db/mysql.py +56 -3
  14. execsql/db/odbc.py +62 -0
  15. execsql/db/postgres.py +1 -1
  16. execsql/db/sqlserver.py +39 -21
  17. execsql/exporters/ods.py +146 -41
  18. execsql/exporters/values.py +166 -9
  19. execsql/importers/base.py +1 -1
  20. execsql/importers/csv.py +1 -1
  21. execsql/importers/ods.py +2 -0
  22. execsql/lsp/diagnostics.py +1 -1
  23. execsql/lsp/document.py +3 -0
  24. execsql/metacommands/io_fileops.py +11 -5
  25. execsql/metacommands/reference.py +1 -1
  26. execsql/metacommands/upsert.py +10 -1
  27. execsql/utils/auth.py +21 -14
  28. execsql/utils/errors.py +1 -1
  29. execsql/utils/paths.py +31 -0
  30. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/README.md +9 -8
  31. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/md_compare.sql +53 -29
  32. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/md_glossary.sql +20 -20
  33. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/md_upsert.sql +59 -66
  34. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/pg_compare.sql +19 -22
  35. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/pg_glossary.sql +13 -17
  36. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/script_template.sql +302 -299
  37. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/ss_compare.sql +39 -29
  38. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/ss_glossary.sql +13 -17
  39. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/ss_upsert.sql +56 -66
  40. {execsql2-2.30.0.dist-info → execsql2-2.31.2.dist-info}/METADATA +17 -41
  41. {execsql2-2.30.0.dist-info → execsql2-2.31.2.dist-info}/RECORD +48 -47
  42. execsql2-2.30.0.data/data/execsql2_extras/pg_upsert.sql +0 -2791
  43. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  44. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  45. {execsql2-2.30.0.data → execsql2-2.31.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
  46. {execsql2-2.30.0.dist-info → execsql2-2.31.2.dist-info}/WHEEL +0 -0
  47. {execsql2-2.30.0.dist-info → execsql2-2.31.2.dist-info}/entry_points.txt +0 -0
  48. {execsql2-2.30.0.dist-info → execsql2-2.31.2.dist-info}/licenses/LICENSE.txt +0 -0
  49. {execsql2-2.30.0.dist-info → execsql2-2.31.2.dist-info}/licenses/NOTICE +0 -0
@@ -30,7 +30,7 @@ __all__ = [
30
30
  "to_snippet",
31
31
  ]
32
32
 
33
- DOCS_URL = "https://execsql2.readthedocs.io/en/latest/reference/"
33
+ DOCS_URL = "https://execsql2.readthedocs.io/en/stable/reference/"
34
34
 
35
35
 
36
36
  @dataclass(frozen=True)
@@ -421,7 +421,7 @@ def _export_failures_if_requested(
421
421
 
422
422
  def _handle_pg_upsert_errors(fn: Any, metacommandline: str | None) -> Any:
423
423
  """Run *fn*, translating pg-upsert exceptions to ErrInfo."""
424
- from pg_upsert import UserCancelledError
424
+ from pg_upsert import PgUpsertError, UserCancelledError
425
425
 
426
426
  try:
427
427
  return fn()
@@ -433,6 +433,15 @@ def _handle_pg_upsert_errors(fn: Any, metacommandline: str | None) -> Any:
433
433
  ) from exc
434
434
  except ErrInfo:
435
435
  raise
436
+ except PgUpsertError as exc:
437
+ # The run was rolled back. The variables describe this failed run,
438
+ # not whatever an earlier PG_UPSERT left in them.
439
+ _set_subvars(exc.result)
440
+ raise ErrInfo(
441
+ "cmd",
442
+ command_text=metacommandline,
443
+ other_msg=f"PG_UPSERT rolled back after a database error: {exc.original}",
444
+ ) from exc
436
445
  except Exception as exc:
437
446
  raise ErrInfo(
438
447
  "exception",
execsql/utils/auth.py CHANGED
@@ -88,22 +88,30 @@ _DEFAULT_PORTS = {"PostgreSQL": 5432, "MySQL": 3306, "SQL Server": 1433, "Oracle
88
88
  _plaintext_warned: bool = False
89
89
 
90
90
 
91
+ def _storing_backend(backend: Any) -> Any:
92
+ """The backend a password saved through *backend* lands in.
93
+
94
+ keyring's ``ChainerBackend`` saves to the first of its backends (highest
95
+ priority first) that accepts it, such as the macOS Keychain ahead of a
96
+ ``keyrings.alt`` file.
97
+ """
98
+ chained = getattr(backend, "backends", None)
99
+ return chained[0] if chained else backend
100
+
101
+
91
102
  def is_plaintext_keyring() -> bool:
92
- """Return True if the active keyring backend stores secrets in cleartext.
93
-
94
- B20/F040: on headless Linux without a real Secret Service, the
95
- ``keyrings.alt.file.PlaintextKeyring`` (or
96
- ``EncryptedKeyring`` with a hard-coded passphrase) backend is
97
- used silently. Detect by inspecting the active backend's module
98
- path so callers can warn the user instead of pretending secrets
99
- are encrypted.
103
+ """Return True if a password stored now would be written in cleartext.
104
+
105
+ That is ``keyrings.alt``'s ``PlaintextKeyring``: on headless Linux with no
106
+ Secret Service, keyring falls back to it silently. Its encrypted
107
+ sibling (a master password protects the file), the Windows backends and
108
+ keyring's ``fail`` placeholder (which stores nothing) are not.
100
109
  """
101
110
  try:
102
111
  import keyring
103
112
 
104
- backend = keyring.get_keyring()
105
- module = type(backend).__module__
106
- return "keyrings.alt" in module or "fail" in module.lower()
113
+ cls = type(_storing_backend(keyring.get_keyring()))
114
+ return cls.__module__.startswith("keyrings.alt") and cls.__name__ == "PlaintextKeyring"
107
115
  except Exception:
108
116
  return False
109
117
 
@@ -118,11 +126,10 @@ def _warn_if_plaintext_keyring() -> None:
118
126
  import keyring
119
127
  import sys
120
128
 
121
- backend_name = type(keyring.get_keyring()).__name__
129
+ backend_name = type(_storing_backend(keyring.get_keyring())).__name__
122
130
  print(
123
131
  f"WARNING: active keyring backend ({backend_name}) stores secrets in "
124
- f"cleartext or with a hard-coded key. Stored passwords are not "
125
- f"meaningfully protected.",
132
+ f"cleartext. Stored passwords are not meaningfully protected.",
126
133
  file=sys.stderr,
127
134
  )
128
135
  except Exception:
execsql/utils/errors.py CHANGED
@@ -37,7 +37,7 @@ __all__ = [
37
37
  "install_hint",
38
38
  ]
39
39
 
40
- _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"
41
41
 
42
42
 
43
43
  def install_hint(extra: str) -> str:
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 "\\/"
@@ -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.
@@ -38,9 +38,10 @@
38
38
  --
39
39
  -- AUTHORS
40
40
  -- Dreas Nielsen (RDN)
41
+ -- Caleb Grant (CG)
41
42
  --
42
43
  -- VERSION
43
- -- 1.0.0
44
+ -- 1.1.0
44
45
  --
45
46
  -- HISTORY
46
47
  -- Date Remarks
@@ -50,13 +51,17 @@
50
51
  -- existence, to eliminate spurious messages from the DBMS.
51
52
  -- Made the 'gls_glossary' table and 'glossary' view non-temporary
52
53
  -- because the 'glossary' view can't reference a temporary table. RDN.
54
+ -- 2026-10-15 execsql2: each script's notes are its docstring (SHOW SCRIPTS, the
55
+ -- language server); def_url is a declared optional parameter;
56
+ -- ADD_GLOSSARY_ITEM keeps def_url; MySQL 8 casts (CHAR, not VARCHAR or ::text). Version 1.1.0. Tested in
57
+ -- tests/templates of the execsql2 repository. CG.
53
58
  -- ============================================================================
54
59
 
55
60
 
56
61
  -- ################################################################
57
62
  -- Script INIT_GLOSSARY
58
63
  -- ===============================================================
59
- --
64
+ -- !x! BEGIN SCRIPT INIT_GLOSSARY with parameters (glossary_db_alias, glossary_table, name_col, def_col, def_url="")
60
65
  -- Initialize the custom glossary and describe the master glossary
61
66
  -- that will provide descriptions to include in the custom glossary.
62
67
  --
@@ -73,14 +78,12 @@
73
78
  -- Optional input argument:
74
79
  -- def_url : The URL of a page that provides additional
75
80
  -- information about the column (or other object).
76
- -- ===============================================================
77
- -- !x! BEGIN SCRIPT INIT_GLOSSARY with parameters (glossary_db_alias, glossary_table, name_col, def_col)
78
81
 
79
82
  -- !x! sub gls_alias !!#glossary_db_alias!!
80
83
  -- !x! sub gls_table !!#glossary_table!!
81
84
  -- !x! sub gls_name !!#name_col!!
82
85
  -- !x! sub gls_def !!#def_col!!
83
- -- !x! if(sub_defined(#def_url))
86
+ -- !x! if(not is_null("!!#def_url!!"))
84
87
  -- !x! sub gls_has_url Yes
85
88
  -- !x! sub gls_url !!#def_url!!
86
89
  -- !x! sub ~url_colspec !!gls_url!! text,
@@ -122,13 +125,11 @@ from gls_glossary order by !!gls_name!!;
122
125
  -- ################################################################
123
126
  -- Script ADD_GLOSSARY_LIST
124
127
  -- ===============================================================
125
- --
128
+ -- !x! BEGIN SCRIPT ADD_GLOSSARY_LIST with parameters (column_list)
126
129
  -- Add a specific list of columns to the glossary.
127
130
  --
128
131
  -- Required input arguments:
129
132
  -- column_list : A string of comma-separated column names.
130
- -- ===============================================================
131
- -- !x! BEGIN SCRIPT ADD_GLOSSARY_LIST with parameters (column_list)
132
133
 
133
134
  -- !x! sub ~entry_alias !!$current_alias!!
134
135
  -- !x! use initial
@@ -230,7 +231,7 @@ select !!gls_collist!! from gls_newglossary;
230
231
  -- ################################################################
231
232
  -- Script ADD_GLOSSARY_ITEM
232
233
  -- ===============================================================
233
- --
234
+ -- !x! BEGIN SCRIPT ADD_GLOSSARY_ITEM with parameters (item, definition, def_url="")
234
235
  -- Add an object name and definition to the custom glossary.
235
236
  -- This does not use the master glossary.
236
237
  --
@@ -242,19 +243,21 @@ select !!gls_collist!! from gls_newglossary;
242
243
  -- Optional input argument:
243
244
  -- def_url : The URL of a page that provides additional
244
245
  -- information about the item.
245
- -- ===============================================================
246
- -- !x! BEGIN SCRIPT ADD_GLOSSARY_ITEM with parameters (item, definition)
247
246
 
248
247
  -- !x! sub ~entry_alias !!$current_alias!!
249
248
  -- !x! use initial
250
249
 
251
- -- !x! if(is_true(gls_has_url))
252
- -- !x! andif(sub_defined(#def_url))
250
+ -- !x! if(is_true(!!gls_has_url!!))
251
+ -- !x! andif(not is_null("!!#def_url!!"))
253
252
  insert into gls_glossary (!!gls_name!!, !!gls_def!!, !!gls_url!!)
254
253
  select
255
254
  inp.item, inp.definition, inp.url
256
255
  from
257
- (select !'!#item!'!::text as item, !'!#definition!'!::text as definition, !'!#def_url!'!::text as url) as inp
256
+ (select
257
+ cast(!'!#item!'! as char(255)) as item,
258
+ cast(!'!#definition!'! as char(255)) as definition,
259
+ cast(!'!#def_url!'! as char(255)) as url
260
+ ) as inp
258
261
  left join gls_glossary as g on g.!!gls_name!! = inp.item
259
262
  where
260
263
  g.!!gls_name!! is null;
@@ -264,8 +267,8 @@ select !!gls_collist!! from gls_newglossary;
264
267
  inp.item, inp.definition
265
268
  from
266
269
  (select
267
- cast(!'!#item!'! as varchar(255)) as item,
268
- cast(!'!#definition!'! as varchar(255)) as definition
270
+ cast(!'!#item!'! as char(255)) as item,
271
+ cast(!'!#definition!'! as char(255)) as definition
269
272
  ) as inp
270
273
  left join gls_glossary as g on g.!!gls_name!! = inp.item
271
274
  where
@@ -285,15 +288,12 @@ select !!gls_collist!! from gls_newglossary;
285
288
  -- ################################################################
286
289
  -- Script ADD_GLOSSARY_TABLE
287
290
  -- ===============================================================
288
- --
291
+ -- !x! BEGIN SCRIPT ADD_GLOSSARY_TABLE with parameters (table)
289
292
  -- Add definitions of all columns in a specified table to the
290
293
  -- custom glossary.
291
294
  --
292
295
  -- Required input arguments:
293
296
  -- table : The table name.
294
- --
295
- -- ===============================================================
296
- -- !x! BEGIN SCRIPT ADD_GLOSSARY_TABLE with parameters (table)
297
297
 
298
298
  -- !x! sub ~entry_alias !!$current_alias!!
299
299