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>
59 lines
2.5 KiB
Python
59 lines
2.5 KiB
Python
from ..connection import connect
|
|
from ..constants import FIELD_TYPES
|
|
from ..quote_ident import quote_ident
|
|
from ..slugify import slugify
|
|
from ..table_name_for import table_name_for
|
|
from .get_definition import get_definition
|
|
from .ensure_field_bounds_schema import ensure_field_bounds_schema
|
|
|
|
|
|
def create_definition(slug, name, fields):
|
|
"""Feature 2 : crée une définition d'objet (comme une table de BDD) et
|
|
exécute le vrai CREATE TABLE correspondant, avec les bons types de
|
|
colonne, y compris les colonnes de clé étrangère pour les relations
|
|
vers d'autres définitions déjà créées dans CE jeu (une définition peut
|
|
donc "en contenir" une autre, au sens relationnel)."""
|
|
ensure_field_bounds_schema(slug)
|
|
conn = connect(slug)
|
|
tname = table_name_for(name)
|
|
|
|
conn.execute(
|
|
"INSERT INTO _definitions (name, table_name) VALUES (?, ?)", (name, tname)
|
|
)
|
|
definition_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
|
|
|
columns_sql = ["id INTEGER PRIMARY KEY AUTOINCREMENT"]
|
|
for pos, f in enumerate(fields):
|
|
fname = slugify(f["name"]).replace("-", "_")
|
|
ftype = f["type"]
|
|
required = 1 if f.get("required") else 0
|
|
relation_definition_id = None
|
|
|
|
if ftype == "relation":
|
|
related = get_definition(slug, int(f["relation_definition_id"]))
|
|
col = f"{fname}_id"
|
|
columns_sql.append(
|
|
f"{quote_ident(col)} INTEGER REFERENCES {related['table_name']}(id)"
|
|
)
|
|
relation_definition_id = related["id"]
|
|
else:
|
|
sql_type = FIELD_TYPES[ftype]["sql"]
|
|
not_null = " NOT NULL" if required else ""
|
|
columns_sql.append(f"{quote_ident(fname)} {sql_type}{not_null}")
|
|
|
|
min_value = f.get("min_value") if ftype in ("nombre_entier", "nombre_decimal") else None
|
|
max_value = f.get("max_value") if ftype in ("nombre_entier", "nombre_decimal") else None
|
|
conn.execute(
|
|
"""INSERT INTO _fields
|
|
(definition_id, name, type, relation_definition_id, required, position, min_value, max_value)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(definition_id, f["name"], ftype, relation_definition_id, required, pos, min_value, max_value),
|
|
)
|
|
|
|
columns_sql.append("created_at TEXT DEFAULT CURRENT_TIMESTAMP")
|
|
create_sql = f"CREATE TABLE {tname} ({', '.join(columns_sql)})"
|
|
conn.execute(create_sql)
|
|
conn.commit()
|
|
conn.close()
|
|
return definition_id
|