Windows installer: payload hash-locking, SBOM, client-IP handling, operator docs

This commit is contained in:
2026-08-03 15:08:20 -04:00
parent ff61a4c5e9
commit 3d43cb3d86
51 changed files with 9002 additions and 190 deletions

View File

@@ -98,6 +98,31 @@ def preflight():
elif version:
ok('MySQL 5.7+/8.0 - no extra index flags needed')
# The database's own default charset. Tables are forced to utf8mb4 by the
# compiler hook (shopdb/utils/mysql_charset.py), so a latin1 default does not
# break the schema - but anything created OUTSIDE that path inherits it, and
# a database created without an explicit CHARSET is the usual cause. Cheap to
# check here, invisible until characters come back mangled.
if version:
try:
row = db.session.execute(text(
'SELECT DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA '
'WHERE SCHEMA_NAME = DATABASE()')).fetchone()
charset = row[0] if row else None
if charset is None:
warn('Could not read the database default charset',
'Not fatal - tables are forced to utf8mb4 regardless.')
elif str(charset).lower() == 'utf8mb4':
ok('Database default charset is utf8mb4')
else:
warn(f'Database default charset is {charset}, not utf8mb4',
'Tables this application creates are forced to utf8mb4, so the '
'schema is correct. Fix the default so anything created outside '
'the migrations matches: ALTER DATABASE <name> CHARACTER SET '
'utf8mb4 COLLATE utf8mb4_unicode_ci;')
except Exception as exc:
warn('Could not read the database default charset', f'({exc})')
click.echo('')
if failures:
click.echo(click.style(f'{len(failures)} blocker(s). Fix them before installing.', fg='red', bold=True))

View File

@@ -34,6 +34,14 @@ from typing import Iterable
from alembic import context
from sqlalchemy import MetaData, pool
# Registers the compiler hook that forces utf8mb4 + DYNAMIC on MySQL. Imported
# for the side effect. It used to live inline in migrations/env.py, so it applied
# to the CORE chain only: a plugin's baseline tables were created at the server's
# default charset while core's were utf8mb4, on the same database. On a server
# defaulting to latin1 that difference is invisible until a join between the two
# stops using an index, or a character comes back mangled.
import shopdb.utils.mysql_charset # noqa: F401,E402
logger = logging.getLogger('alembic.env.plugin')
# Explicit table-ownership map. Adding tables to a plugin requires updating

View File

@@ -0,0 +1,40 @@
"""Force every table created on MySQL to utf8mb4 + DYNAMIC row format.
Importing this module installs a SQLAlchemy compiler hook. It is a side effect on
purpose: the hook has to be registered before any CreateTable is compiled, and
every place that creates tables needs it.
WHY. Without it a CREATE TABLE inherits the SERVER's default charset. A MySQL box
defaulting to latin1 - common on older installs, and the West Jefferson 5.6
server is one - silently builds a latin1 schema that drifts from the utf8mb4
production target. Nothing fails at create time; it surfaces later as mangled
characters, or as a join between a utf8mb4 and a latin1 column that cannot use an
index.
DYNAMIC row format is the other half: it keeps utf8mb4 indexes under the 767-byte
prefix limit on pre-5.7 InnoDB, which is what error 1071 ("key too long") is.
This lived inline in migrations/env.py, so it covered the CORE chain only. Plugin
chains (ADR-008) run through shopdb/plugins/alembic_template.py, which never
imported it - so a plugin's baseline tables were created at the server default
while core's were utf8mb4, on the same database. Both import this now.
Scoped to the mysql dialect, so the SQLite test database is untouched.
"""
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.schema import CreateTable
TABLE_SUFFIX = (
" ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
" COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC"
)
@compiles(CreateTable, "mysql")
def _mysql_create_table_utf8mb4(element, compiler, **kw):
sql = compiler.visit_create_table(element, **kw)
# An explicit CHARSET in the model's __table_args__ wins - this is a default,
# not an override.
if "CHARSET" not in sql.upper():
sql = sql.rstrip().rstrip(";") + TABLE_SUFFIX
return sql