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
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

View File

@ -158,10 +158,15 @@ class CmbDB(GObject.GObject):
def history_delete(self, table, table_pk):
self.execute(self.__history_commands[table]["DELETE"], table_pk)
def history_update(self, table, column, table_pk, values):
command = self.__history_commands[table]["UPDATE"].format(column=column)
i = self.__table_column_mapping[table][column]
self.execute(command, [values[i]] + table_pk)
def history_update(self, table, columns, table_pk, values):
update_command = self.__history_commands[table]["UPDATE"]
set_expression = f"({','.join(columns)}) = ({','.join(['?' for i in columns])})"
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):
# Get table columns
@ -185,6 +190,7 @@ class CmbDB(GObject.GObject):
"""
self.__clear_history = clear_history
# Collect Column info
i = 0
column_mapping = {}
for row in c.execute(f"PRAGMA table_info({table});"):
@ -200,6 +206,28 @@ class CmbDB(GObject.GObject):
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
self.__table_column_mapping[table] = column_mapping
@ -215,7 +243,7 @@ class CmbDB(GObject.GObject):
self.__history_commands[table] = {
"DELETE": f"DELETE FROM {table} WHERE ({pkcolumns}) IS ({pkcolumns_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
@ -249,8 +277,43 @@ class CmbDB(GObject.GObject):
if len(pk_columns) == 0:
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
for column in non_pk_columns:
if column in unique_constraints_flat:
continue
c.execute(
f"""
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})
OR
(
(SELECT command, table_name, column_name FROM history WHERE history_id = {history_seq})
IS NOT ('UPDATE', '{table}', '{column}')
(SELECT command, table_name, columns FROM history WHERE history_id = {history_seq})
IS NOT ('UPDATE', '{table}', json_array('{column}'))
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)
)
)
BEGIN
{clear_history};
INSERT INTO history (history_id, command, table_name, column_name, 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}));
INSERT INTO history (history_id, command, table_name, columns, table_pk, new_values, 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;
"""
)
@ -282,8 +345,8 @@ class CmbDB(GObject.GObject):
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})
AND
(SELECT command, table_name, column_name FROM history WHERE history_id = {history_seq})
IS ('UPDATE', '{table}', '{column}')
(SELECT command, table_name, columns FROM history WHERE history_id = {history_seq})
IS ('UPDATE', '{table}', json_array('{column}'))
BEGIN
UPDATE history SET new_values=json_array({new_values}) WHERE history_id = {history_seq};
END;
@ -297,7 +360,7 @@ class CmbDB(GObject.GObject):
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})
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)
BEGIN
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(
"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

View File

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

View File

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

View File

@ -14,6 +14,15 @@
</p>
</description>
<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">
<description>
<p>Accessibility Release!</p>

View File

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