Store the policy JSON columns as TEXT for MySQL 5.6

The branch stored scopejson, desiredstate and params as sa.JSON, the only JSON
columns in the product. MySQL 5.6 has no JSON type - it arrived in 5.7 - and the
migration failed there with a bare syntax error near the word JSON.

They are TEXT now, through a JSONText type decorator, so the seventeen call sites
still assign and read dicts. The convention elsewhere is already TEXT:
guides.blocksjson, geenforce.manifestjson.

Spec regenerated: 331 paths, 477 operations across 22 surfaces.
This commit is contained in:
2026-08-28 16:18:34 -04:00
parent 80bb7b5ba1
commit 7bda8e6d60
4 changed files with 1668 additions and 6 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,14 @@ branch_labels = None
depends_on = None
# TEXT rather than sa.JSON on purpose. MySQL 5.6 has no JSON type - it arrived
# in 5.7 - and this migration failed there with a bare syntax error near the
# word JSON. Every other JSON-bearing column in the product is TEXT for the same
# reason (guides.blocksjson, geenforce.manifestjson); the model reads and writes
# dicts through the JSONText type decorator, so nothing above the database
# changes.
def upgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
@@ -33,7 +41,7 @@ def upgrade():
sa.Column('configurationpolicyid', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=150), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('scopejson', sa.JSON(), nullable=True),
sa.Column('scopejson', sa.Text(), nullable=True),
sa.Column('source', sa.String(length=50), nullable=False,
server_default='local'),
sa.Column('createddate', sa.DateTime(), nullable=False),
@@ -57,8 +65,8 @@ def upgrade():
sa.Column('action', sa.String(length=100), nullable=False),
sa.Column('mode', sa.String(length=20), nullable=False,
server_default='gate'),
sa.Column('desiredstate', sa.JSON(), nullable=True),
sa.Column('params', sa.JSON(), nullable=True),
sa.Column('desiredstate', sa.Text(), nullable=True),
sa.Column('params', sa.Text(), nullable=True),
sa.Column('intervalminutes', sa.Integer(), nullable=True),
sa.Column('sortorder', sa.Integer(), nullable=False,
server_default='0'),

View File

@@ -0,0 +1,44 @@
"""JSON stored in a TEXT column.
MySQL 5.6 has no JSON type - it arrived in 5.7 - and a site is still running
5.6, where `sa.JSON()` fails at migration time with a bare syntax error near the
word JSON. Every other JSON-bearing column in this product is already TEXT for
that reason: guides' `blocksjson`, GE-Enforce's `manifestjson`.
This keeps the Python side identical. A model attribute still holds a dict or a
list, still assigns and reads like one; only the storage changes. The
alternative - json.dumps and json.loads at seventeen call sites - is the same
thing written out seventeen times, with seventeen chances to forget one.
Note the SQLite behaviour is deliberately the same as MySQL's. Tests run on
SQLite and production does not, so a type that behaved differently across the
two would hide exactly the failure this exists to prevent.
"""
import json
from sqlalchemy import Text
from sqlalchemy.types import TypeDecorator
class JSONText(TypeDecorator):
"""A dict or list on the Python side, TEXT in the database."""
impl = Text
cache_ok = True
def process_bind_param(self, value, dialect):
if value is None:
return None
return json.dumps(value, separators=(',', ':'))
def process_result_value(self, value, dialect):
if value is None or value == '':
return None
try:
return json.loads(value)
except (TypeError, ValueError):
# A row written before this column was JSON, or hand-edited. Hand
# back the raw text rather than raising: a policy that cannot be
# parsed should be visible and fixable, not an exception on read.
return value

View File

@@ -18,6 +18,7 @@ every table for a clean rollback.
"""
from shopdb.api import db, BaseModel
from .jsontext import JSONText
# Provenance of a policy row.
@@ -43,7 +44,7 @@ class ConfigurationPolicy(BaseModel):
# Boolean target AST: {"type":"and","nodes":[{"type":"pctype","id":3},
# {"type":"not","node":{"type":"app","id":12}}]}. Validated server-side
# (known node types, depth cap) before it is stored - see policyscope.
scopejson = db.Column(db.JSON, nullable=True)
scopejson = db.Column(JSONText, nullable=True)
# GE-Enforce interop seam (see POLICY_SOURCE_LOCAL).
source = db.Column(db.String(50), default=POLICY_SOURCE_LOCAL, nullable=False)
@@ -101,11 +102,11 @@ class ConfigurationPolicyAction(BaseModel):
# Desired-state target for mode=desired, as JSON so different actions can
# express different targets (e.g. {"running": true}). Null for gate mode.
desiredstate = db.Column(db.JSON, nullable=True)
desiredstate = db.Column(JSONText, nullable=True)
# Optional action parameters passed through to the command (JSON), and an
# optional reconcile cadence in minutes (null = reconcile on every report).
params = db.Column(db.JSON, nullable=True)
params = db.Column(JSONText, nullable=True)
intervalminutes = db.Column(db.Integer, nullable=True)
sortorder = db.Column(db.Integer, default=0, nullable=False)