Reproduit à l'identique le cas signalé (objet "dialog" avec les champs
order/spiker/text/level_id/parcour_id) : le champ "order" est un mot
réservé SQL — "CREATE TABLE dialog (order INTEGER, ...)" plante avec
"OperationalError: near \"order\": syntax error". Comme ce crash survient
APRÈS l'INSERT de la ligne _definitions mais AVANT le commit(), rien
n'était jamais persisté : l'objet ENTIER disparaissait, malgré des champs
parfaitement remplis — d'où "j'ai tout rempli comme il faut et aucun objet
n'est créé". Mon précédent correctif (champ "Relation" sans cible) était
réel mais ne couvrait pas ce cas précis.
Cause de fond : chaque nom de colonne (dérivé du nom de champ tapé par
l'utilisateur, via slugify) était interpolé TEL QUEL dans du SQL brut
(CREATE TABLE, INSERT, UPDATE, ALTER TABLE ADD/DROP/RENAME COLUMN) sans
jamais être encadré de guillemets — n'importe quel nom de champ qui soit
aussi un mot réservé SQLite (order, group, index, select, where, table,
key, default, check, references, unique...) déclenchait exactement le
même crash-et-perte-de-transaction, dans n'importe laquelle de ces
opérations.
Correctif général (pas un simple contournement pour "order") :
db/quote_ident.py encadre tout identifiant de colonne de guillemets
doubles (forme standard SQL, supportée par SQLite) — appliqué partout où
un nom de colonne utilisateur est interpolé dans du SQL brut :
create_definition, add_field_to_definition, delete_field, update_field
(RENAME COLUMN), insert_row, update_row, update_row_field,
rows_referencing. Les noms de TABLE n'ont pas besoin de cette protection
(table_name_for.py les préfixe toujours "obj_", donc jamais un mot réservé
à eux seuls).
Trois nouveaux tests (tests/test_reserved_sql_keyword_field_names.py) :
création avec un champ "order" + insertion/lecture/mise à jour d'une
ligne, renommage d'un champ vers/depuis un mot réservé ("group"), ajout
d'un champ "select" à un objet existant — les trois confirmés en échec
sur l'ancien code (même erreur reproduite) puis au vert avec le
correctif. 136 tests au vert au total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
50 lines
2.3 KiB
Python
50 lines
2.3 KiB
Python
import sqlite3
|
|
|
|
from ..connection import connect
|
|
from ..quote_ident import quote_ident
|
|
from ..slugify import slugify
|
|
from .get_definition import get_definition
|
|
|
|
|
|
def update_field(slug, definition_id, field_id, new_name, required, relation_definition_id=None, min_value=None, max_value=None):
|
|
"""CRUD — Update d'une définition : modifie un champ déjà créé (nom,
|
|
obligatoire, objet lié si c'est une relation, et bornes mini/maxi si
|
|
c'est un champ numérique — voir 2.2, bornage automatique). Si le nom
|
|
change, la vraie colonne SQL est renommée (ALTER TABLE ... RENAME
|
|
COLUMN, SQLite ≥ 3.25) pour que la colonne réelle continue de
|
|
correspondre exactement au nom du champ tel qu'affiché — pas de dérive
|
|
entre la définition et la table. Le TYPE d'un champ existant ne se
|
|
change pas ici (une vraie conversion de type SQLite demanderait de
|
|
reconstruire la table et de convertir les données déjà enregistrées,
|
|
hors scope de cette version)."""
|
|
definition = get_definition(slug, definition_id)
|
|
field = next((f for f in definition["fields"] if f["id"] == field_id), None)
|
|
if not field:
|
|
return
|
|
|
|
old_col = slugify(field["name"]).replace("-", "_")
|
|
new_col = slugify(new_name).replace("-", "_")
|
|
if field["type"] == "relation":
|
|
old_col += "_id"
|
|
new_col += "_id"
|
|
|
|
conn = connect(slug)
|
|
if old_col != new_col:
|
|
try:
|
|
conn.execute(
|
|
f"ALTER TABLE {definition['table_name']} RENAME COLUMN {quote_ident(old_col)} TO {quote_ident(new_col)}"
|
|
)
|
|
except sqlite3.OperationalError:
|
|
pass # SQLite trop ancien pour RENAME COLUMN : la colonne SQL garde son ancien nom
|
|
|
|
rel_id = int(relation_definition_id) if (field["type"] == "relation" and relation_definition_id) else field["relation_definition_id"]
|
|
is_numeric = field["type"] in ("nombre_entier", "nombre_decimal")
|
|
min_v = (min_value if min_value not in (None, "") else None) if is_numeric else None
|
|
max_v = (max_value if max_value not in (None, "") else None) if is_numeric else None
|
|
conn.execute(
|
|
"UPDATE _fields SET name = ?, required = ?, relation_definition_id = ?, min_value = ?, max_value = ? WHERE id = ?",
|
|
(new_name, 1 if required else 0, rel_id, min_v, max_v, field_id),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|