Compare commits

...

2 Commits
main ... 0.94.1

Author SHA1 Message Date
Juan Pablo Ugarte
4f7272b2f9
Rolling 0.94.1 2024-12-13 17:35:07 -05:00
Juan Pablo Ugarte
eea5ff18ea
Data Model: improve history table
- Replace column_name TEXT with columns JSON

This allows to keep track of updates of multiple columns at the same
time, which is needed for unique indexes with more than one column.

This fixes error undoing a parent change like in D&D
2024-12-13 16:17:21 -05:00
7 changed files with 134 additions and 43 deletions

View File

@ -8,6 +8,17 @@ packaging changes like new dependencies or build system changes.
Cambalache used even/odd minor numbers to differentiate between stable and Cambalache used even/odd minor numbers to differentiate between stable and
development releases. development releases.
## 0.94.1
2024-11-26 - First bugfix release in the series!
- Fix issue with undoing a parent change.
- Fix workspace initialization error
- Fix warning on undo/redo message generation
### Issues
- #253 "Error updating UI 1: gtk-builder-error-quark: .:8:1 Invalid object type 'AdwApplicationWindow' (6)"
## 0.94.0 ## 0.94.0

View File

@ -158,10 +158,15 @@ class CmbDB(GObject.GObject):
def history_delete(self, table, table_pk): def history_delete(self, table, table_pk):
self.execute(self.__history_commands[table]["DELETE"], table_pk) self.execute(self.__history_commands[table]["DELETE"], table_pk)
def history_update(self, table, column, table_pk, values): def history_update(self, table, columns, table_pk, values):
command = self.__history_commands[table]["UPDATE"].format(column=column) update_command = self.__history_commands[table]["UPDATE"]
i = self.__table_column_mapping[table][column] set_expression = f"({','.join(columns)}) = ({','.join(['?' for i in columns])})"
self.execute(command, [values[i]] + table_pk)
exp_vals = []
for col in columns:
exp_vals.append(values[self.__table_column_mapping[table][col]])
self.execute(update_command.format(set_expression=set_expression), exp_vals + table_pk)
def __create_history_triggers(self, c, table): def __create_history_triggers(self, c, table):
# Get table columns # Get table columns
@ -185,6 +190,7 @@ class CmbDB(GObject.GObject):
""" """
self.__clear_history = clear_history self.__clear_history = clear_history
# Collect Column info
i = 0 i = 0
column_mapping = {} column_mapping = {}
for row in c.execute(f"PRAGMA table_info({table});"): for row in c.execute(f"PRAGMA table_info({table});"):
@ -200,6 +206,28 @@ class CmbDB(GObject.GObject):
i += 1 i += 1
# Collect unique constraint indexes
unique_constraints_indexes = []
for row in c.execute(f"PRAGMA index_list({table});"):
n, name, is_unique, index_type, is_partial = row
if is_unique and index_type == "u":
unique_constraints_indexes.append(name)
# Collect unique constraints indexes with more than one non pk column
unique_constraints = []
for index_name in unique_constraints_indexes:
index_columns = []
for row in c.execute(f"PRAGMA index_info({index_name});"):
index_rank, table_rank, name = row
if name not in pk_columns:
index_columns.append(name)
if len(index_columns) > 1:
unique_constraints.append(index_columns)
unique_constraints_flat = [i for constraints in unique_constraints for i in constraints]
# Map column index to column name # Map column index to column name
self.__table_column_mapping[table] = column_mapping self.__table_column_mapping[table] = column_mapping
@ -215,7 +243,7 @@ class CmbDB(GObject.GObject):
self.__history_commands[table] = { self.__history_commands[table] = {
"DELETE": f"DELETE FROM {table} WHERE ({pkcolumns}) IS ({pkcolumns_format});", "DELETE": f"DELETE FROM {table} WHERE ({pkcolumns}) IS ({pkcolumns_format});",
"INSERT": f"INSERT INTO {table} ({columns}) VALUES ({columns_format});", "INSERT": f"INSERT INTO {table} ({columns}) VALUES ({columns_format});",
"UPDATE": f"UPDATE {table} SET {{column}} = ? WHERE ({pkcolumns}) IS ({pkcolumns_format});", "UPDATE": f"UPDATE {table} SET {{set_expression}} WHERE ({pkcolumns}) IS ({pkcolumns_format});",
} }
# INSERT Trigger # INSERT Trigger
@ -249,8 +277,43 @@ class CmbDB(GObject.GObject):
if len(pk_columns) == 0: if len(pk_columns) == 0:
return return
# UPDATE Trigger for each non PK column unique indexes
for columns in unique_constraints:
underscore_columns = "_".join(columns)
colon_columns = ",".join(columns)
new_columns = ",".join(f"NEW.{col}" for col in columns)
old_columns = ",".join(f"OLD.{col}" for col in columns)
string_columns = ",".join(f"'{col}'" for col in columns)
c.execute(
f"""
CREATE TRIGGER on_{table}_update_{underscore_columns} AFTER UPDATE OF {colon_columns} ON {table}
WHEN
({new_columns}) IS NOT ({old_columns}) AND {history_is_enabled} AND
(
(SELECT table_pk FROM history WHERE history_id = {history_seq}) IS NOT json_array({old_pk_values})
OR
(
(SELECT command, table_name, columns FROM history WHERE history_id = {history_seq})
IS NOT ('UPDATE', '{table}', json_array({string_columns}))
AND
(SELECT command, table_name, columns FROM history WHERE history_id = {history_seq})
IS NOT ('INSERT', '{table}', NULL)
)
)
BEGIN
{clear_history};
INSERT INTO history (history_id, command, table_name, columns, table_pk, new_values, old_values)
VALUES ({history_next_seq}, 'UPDATE', '{table}', json_array({string_columns}), json_array({new_pk_values}), json_array({new_values}), json_array({old_values}));
END;
"""
)
# UPDATE Trigger for each non PK column # UPDATE Trigger for each non PK column
for column in non_pk_columns: for column in non_pk_columns:
if column in unique_constraints_flat:
continue
c.execute( c.execute(
f""" f"""
CREATE TRIGGER on_{table}_update_{column} AFTER UPDATE OF {column} ON {table} CREATE TRIGGER on_{table}_update_{column} AFTER UPDATE OF {column} ON {table}
@ -260,17 +323,17 @@ class CmbDB(GObject.GObject):
(SELECT table_pk FROM history WHERE history_id = {history_seq}) IS NOT json_array({old_pk_values}) (SELECT table_pk FROM history WHERE history_id = {history_seq}) IS NOT json_array({old_pk_values})
OR OR
( (
(SELECT command, table_name, column_name FROM history WHERE history_id = {history_seq}) (SELECT command, table_name, columns FROM history WHERE history_id = {history_seq})
IS NOT ('UPDATE', '{table}', '{column}') IS NOT ('UPDATE', '{table}', json_array('{column}'))
AND AND
(SELECT command, table_name, column_name FROM history WHERE history_id = {history_seq}) (SELECT command, table_name, columns FROM history WHERE history_id = {history_seq})
IS NOT ('INSERT', '{table}', NULL) IS NOT ('INSERT', '{table}', NULL)
) )
) )
BEGIN BEGIN
{clear_history}; {clear_history};
INSERT INTO history (history_id, command, table_name, column_name, table_pk, new_values, old_values) INSERT INTO history (history_id, command, table_name, columns, table_pk, new_values, old_values)
VALUES ({history_next_seq}, 'UPDATE', '{table}', '{column}', json_array({new_pk_values}), json_array({new_values}), json_array({old_values})); VALUES ({history_next_seq}, 'UPDATE', '{table}', json_array('{column}'), json_array({new_pk_values}), json_array({new_values}), json_array({old_values}));
END; END;
""" """
) )
@ -282,8 +345,8 @@ class CmbDB(GObject.GObject):
NEW.{column} IS NOT OLD.{column} AND {history_is_enabled} AND NEW.{column} IS NOT OLD.{column} AND {history_is_enabled} AND
(SELECT table_pk FROM history WHERE history_id = {history_seq}) IS json_array({old_pk_values}) (SELECT table_pk FROM history WHERE history_id = {history_seq}) IS json_array({old_pk_values})
AND AND
(SELECT command, table_name, column_name FROM history WHERE history_id = {history_seq}) (SELECT command, table_name, columns FROM history WHERE history_id = {history_seq})
IS ('UPDATE', '{table}', '{column}') IS ('UPDATE', '{table}', json_array('{column}'))
BEGIN BEGIN
UPDATE history SET new_values=json_array({new_values}) WHERE history_id = {history_seq}; UPDATE history SET new_values=json_array({new_values}) WHERE history_id = {history_seq};
END; END;
@ -297,7 +360,7 @@ class CmbDB(GObject.GObject):
NEW.{column} IS NOT OLD.{column} AND {history_is_enabled} AND NEW.{column} IS NOT OLD.{column} AND {history_is_enabled} AND
(SELECT table_pk FROM history WHERE history_id = {history_seq}) IS json_array({old_pk_values}) (SELECT table_pk FROM history WHERE history_id = {history_seq}) IS json_array({old_pk_values})
AND AND
(SELECT command, table_name, column_name FROM history WHERE history_id = {history_seq}) (SELECT command, table_name, columns FROM history WHERE history_id = {history_seq})
IS ('INSERT', '{table}', NULL) IS ('INSERT', '{table}', NULL)
BEGIN BEGIN
UPDATE history SET new_values=json_array({new_values}) WHERE history_id = {history_seq}; UPDATE history SET new_values=json_array({new_values}) WHERE history_id = {history_seq};

View File

@ -335,7 +335,7 @@ class CmbObject(CmbBaseObject, Gio.ListModel):
project.db.execute( project.db.execute(
"UPDATE object SET parent_id=?, position=? WHERE ui_id=? AND object_id=?;", "UPDATE object SET parent_id=?, position=? WHERE ui_id=? AND object_id=?;",
(new_parent_id, new_position, ui_id, object_id) (new_parent_id, new_position or 0, ui_id, object_id)
) )
# Update children positions in old parent # Update children positions in old parent

View File

@ -418,6 +418,9 @@ class CmbProject(GObject.Object, Gio.ListModel):
return n_files return n_files
def __selection_remove(self, obj): def __selection_remove(self, obj):
if obj not in self.__selection:
return
try: try:
self.__selection.remove(obj) self.__selection.remove(obj)
except Exception: except Exception:
@ -790,11 +793,12 @@ class CmbProject(GObject.Object, Gio.ListModel):
c = self.db.cursor() c = self.db.cursor()
# Get last command # Get last command
command, range_id, table, column, table_pk, old_values, new_values = c.execute( command, range_id, table, columns, table_pk, old_values, new_values = c.execute(
"SELECT command, range_id, table_name, column_name, table_pk, old_values, new_values FROM history WHERE history_id=?", "SELECT command, range_id, table_name, columns, table_pk, old_values, new_values FROM history WHERE history_id=?",
(self.history_index, ) (self.history_index,),
).fetchone() ).fetchone()
columns = json.loads(columns) if columns else None
table_pk = json.loads(table_pk) if table_pk else None table_pk = json.loads(table_pk) if table_pk else None
old_values = json.loads(old_values) if old_values else None old_values = json.loads(old_values) if old_values else None
new_values = json.loads(new_values) if new_values else None new_values = json.loads(new_values) if new_values else None
@ -814,7 +818,7 @@ class CmbProject(GObject.Object, Gio.ListModel):
else: else:
self.db.history_insert(table, new_values) self.db.history_insert(table, new_values)
self.__undo_redo_update_insert_delete(c, undo, command, table, column, table_pk, old_values, new_values) self.__undo_redo_update_insert_delete(c, undo, command, table, columns, table_pk, old_values, new_values)
elif command == "DELETE": elif command == "DELETE":
if table == "object": if table == "object":
parent, position = get_object_position(c, old_values) parent, position = get_object_position(c, old_values)
@ -829,10 +833,10 @@ class CmbProject(GObject.Object, Gio.ListModel):
else: else:
self.db.history_delete(table, table_pk) self.db.history_delete(table, table_pk)
self.__undo_redo_update_insert_delete(c, undo, command, table, column, table_pk, old_values, new_values) self.__undo_redo_update_insert_delete(c, undo, command, table, columns, table_pk, old_values, new_values)
elif command == "UPDATE": elif command == "UPDATE":
# Ignore parent_id since its changed together with position # parent_id and position have to change together because their are part of a unique index
if update_objects is not None and table == "object" and column == "position": if update_objects is not None and table == "object" and "position" in columns and "parent_id" in columns:
old_parent, old_position = get_object_position(c, old_values) old_parent, old_position = get_object_position(c, old_values)
new_parent, new_position = get_object_position(c, new_values) new_parent, new_position = get_object_position(c, new_values)
@ -848,11 +852,11 @@ class CmbProject(GObject.Object, Gio.ListModel):
update_objects.append((old_parent, old_position, 1, 0)) update_objects.append((old_parent, old_position, 1, 0))
if undo: if undo:
self.db.history_update(table, column, table_pk, old_values) self.db.history_update(table, columns, table_pk, old_values)
else: else:
self.db.history_update(table, column, table_pk, new_values) self.db.history_update(table, columns, table_pk, new_values)
self.__undo_redo_update_update(c, undo, command, table, column, table_pk, old_values, new_values) self.__undo_redo_update_update(c, undo, command, table, columns, table_pk, old_values, new_values)
elif command == "PUSH" or command == "POP": elif command == "PUSH" or command == "POP":
pass pass
else: else:
@ -860,15 +864,14 @@ class CmbProject(GObject.Object, Gio.ListModel):
c.close() c.close()
def __undo_redo_update_update(self, c, undo, command, table, column, pk, old_values, new_values): def __undo_redo_update_update(self, c, undo, command, table, columns, pk, old_values, new_values):
if table is None: if table is None or command != "UPDATE":
return return
for column in columns:
# Update tree model and emit signals # Update tree model and emit signals
# We can not easily implement this using triggers because they are called # We can not easily implement this using triggers because they are called
# even if the transaction is rollback because of a FK constraint # even if the transaction is rollback because of a FK constraint
if command == "UPDATE":
if table == "object": if table == "object":
obj = self.get_object_by_id(pk[0], pk[1]) obj = self.get_object_by_id(pk[0], pk[1])
if obj: if obj:
@ -911,7 +914,7 @@ class CmbProject(GObject.Object, Gio.ListModel):
if obj: if obj:
obj.notify(column) obj.notify(column)
def __undo_redo_update_insert_delete(self, c, undo, command, table, column, pk, old_values, new_values): def __undo_redo_update_insert_delete(self, c, undo, command, table, columns, pk, old_values, new_values):
if table is None: if table is None:
return return
@ -990,9 +993,8 @@ class CmbProject(GObject.Object, Gio.ListModel):
def __undo_redo(self, undo): def __undo_redo(self, undo):
selection = self.get_selection() selection = self.get_selection()
command, range_id, table, column = self.db.execute( command, range_id, table = self.db.execute(
"SELECT command, range_id, table_name, column_name FROM history WHERE history_id=?", "SELECT command, range_id, table_name FROM history WHERE history_id=?", (self.history_index,)
(self.history_index, )
).fetchone() ).fetchone()
update_parents = [] update_parents = []
@ -1206,16 +1208,18 @@ class CmbProject(GObject.Object, Gio.ListModel):
def get_msg(index): def get_msg(index):
cmd = c.execute( cmd = c.execute(
""" """
SELECT command, range_id, table_name, column_name, message, old_values, new_values SELECT command, range_id, table_name, columns, message, old_values, new_values
FROM history FROM history
WHERE history_id=? WHERE history_id=?
""", """,
(index,) (index,),
).fetchone() ).fetchone()
if cmd is None: if cmd is None:
return None return None
command, range_id, table, column, message, old_values, new_values = cmd command, range_id, table, columns, message, old_values, new_values = cmd
columns = json.loads(columns) if columns else []
if message is not None: if message is not None:
return message return message
@ -1283,10 +1287,14 @@ class CmbProject(GObject.Object, Gio.ListModel):
.get(command, None) .get(command, None)
) )
if msg is not None: if msg is None:
msg = msg.format(**get_msg_vars(table, column, values)) return None
return msg msgs = []
for column in columns:
msgs.append(msg.format(**get_msg_vars(table, column, values)))
return "\n".join(msgs) if len(msgs) > 1 else (msgs[0] if msgs else None)
undo_msg = get_msg(self.history_index) undo_msg = get_msg(self.history_index)
redo_msg = get_msg(self.history_index + 1) redo_msg = get_msg(self.history_index + 1)

View File

@ -41,7 +41,7 @@ CREATE TABLE history (
command TEXT NOT NULL, command TEXT NOT NULL,
range_id INTEGER REFERENCES history, range_id INTEGER REFERENCES history,
table_name TEXT, table_name TEXT,
column_name TEXT, columns JSON,
message TEXT, message TEXT,
table_pk JSON, table_pk JSON,
new_values JSON, new_values JSON,

View File

@ -14,6 +14,15 @@
</p> </p>
</description> </description>
<releases> <releases>
<release date="2024-12-13" version="0.94.1">
<description>
<p>First bugfix release in the series!</p>
<ul>
<li>Fix issue with undoing a parent change.</li>
<li>Fix workspace initialization error (#253)</li>
</ul>
</description>
</release>
<release date="2024-11-27" version="0.94.0"> <release date="2024-11-27" version="0.94.0">
<description> <description>
<p>Accessibility Release!</p> <p>Accessibility Release!</p>

View File

@ -1,6 +1,6 @@
project( project(
'cambalache', 'c', 'cambalache', 'c',
version: '0.94.0', version: '0.94.1',
meson_version: '>= 0.64.0', meson_version: '>= 0.64.0',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',