diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 59be57558a..5b1220cd17 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Allow creation of foreign key on non primary key * Include context information in access error * Remove trytond-worker max tasks per child option for Python <= 3.10 * Do not copy readonly relational fields diff --git a/trytond/doc/ref/backend.rst b/trytond/doc/ref/backend.rst index f9f2816d1b..19906ea800 100644 --- a/trytond/doc/ref/backend.rst +++ b/trytond/doc/ref/backend.rst @@ -279,15 +279,16 @@ TableHandler The ``default`` is a method that return the value to fill the new column. ``comment`` set as comment for the column. -.. method:: TableHandler.add_fk(column_name, reference[, on_delete]) +.. method:: TableHandler.add_fk(columns, reference[, ref_columns[, on_delete]) - Add a foreign key constraint on the named column to target the ``reference`` - table name. + Add a foreign key constraint on the named columns to target the + ``ref_columns`` of ``reference`` table name. ``on_delete`` defines the method to use when foreign record is deleted. -.. method:: TableHandler.drop_fk(column_name[, table]) +.. method:: TableHandler.drop_fk(columns[, ref_columns[, table]) - Drop the foreign key constrant on the named column. + Drop the foreign key constraint on the named columns targeting the + ``ref_columns``. ``table`` can be used to alter another table. .. method:: TableHandler.not_null_action(column_name[, action]) diff --git a/trytond/trytond/backend/postgresql/table.py b/trytond/trytond/backend/postgresql/table.py index 1f68327e22..e264dacc97 100644 --- a/trytond/trytond/backend/postgresql/table.py +++ b/trytond/trytond/backend/postgresql/table.py @@ -396,40 +396,63 @@ class TableHandler(TableHandlerInterface): self._update_definitions(columns=True) - def add_fk(self, column_name, reference, on_delete=None): + def add_fk(self, columns, reference, ref_columns=None, on_delete=None): if on_delete is not None: on_delete = on_delete.upper() else: on_delete = 'SET NULL' + if isinstance(columns, str): + columns = [columns] cursor = Transaction().connection.cursor() - name = self.convert_name(self.table_name + '_' + column_name + '_fkey') + if ref_columns: + ref_columns_name = '_' + '_'.join(ref_columns) + else: + ref_columns_name = '' + name = self.convert_name( + self.table_name + '_' + '_'.join(columns) + + ref_columns_name + '_fkey') if name in self._constraints: - if self._fk_deltypes.get(column_name) != on_delete: - self.drop_fk(column_name) - add = True + for column_name in columns: + if self._fk_deltypes.get(column_name) != on_delete: + self.drop_fk(columns, ref_columns) + add = True + break else: add = False else: add = True if add: + columns = SQL(', ').join(map(Identifier, columns)) + if not ref_columns: + ref_columns = ['id'] + ref_columns = SQL(', ').join(map(Identifier, ref_columns)) cursor.execute( SQL( "ALTER TABLE {table} " "ADD CONSTRAINT {constraint} " - "FOREIGN KEY ({column}) REFERENCES {reference} " + "FOREIGN KEY ({columns}) " + "REFERENCES {reference} ({ref_columns}) " "ON DELETE {action}" ) .format( table=Identifier(self.table_name), constraint=Identifier(name), - column=Identifier(column_name), + columns=columns, reference=Identifier(reference), + ref_columns=ref_columns, action=SQL(on_delete))) self._update_definitions(constraints=True) - def drop_fk(self, column_name, table=None): - self.drop_constraint(column_name + '_fkey', table=table) + def drop_fk(self, columns, ref_columns=None, table=None): + if isinstance(columns, str): + columns = [columns] + if ref_columns: + ref_columns_name = '_' + '_'.join(ref_columns) + else: + ref_columns_name = '' + self.drop_constraint( + '_'.join(columns) + ref_columns_name + '_fkey', table=table) def not_null_action(self, column_name, action='add'): if not self.column_exist(column_name): diff --git a/trytond/trytond/backend/sqlite/table.py b/trytond/trytond/backend/sqlite/table.py index 8558c1361a..d1c285ea4c 100644 --- a/trytond/trytond/backend/sqlite/table.py +++ b/trytond/trytond/backend/sqlite/table.py @@ -255,10 +255,10 @@ class TableHandler(TableHandlerInterface): self._update_definitions(columns=True) - def add_fk(self, column_name, reference, on_delete=None): + def add_fk(self, columns, reference, ref_columns=None, on_delete=None): warnings.warn('Unable to add foreign key with SQLite backend') - def drop_fk(self, column_name, table=None): + def drop_fk(self, columns=None, ref_columns=None, table=None): warnings.warn('Unable to drop foreign key with SQLite backend') def not_null_action(self, column_name, action='add'): diff --git a/trytond/trytond/backend/table.py b/trytond/trytond/backend/table.py index fd252f3d5d..cdcdf2f119 100644 --- a/trytond/trytond/backend/table.py +++ b/trytond/trytond/backend/table.py @@ -70,10 +70,10 @@ class TableHandlerInterface(object): def add_column(self, column_name, abstract_type, default=None, comment=''): raise NotImplementedError - def add_fk(self, column_name, reference, on_delete=None): + def add_fk(self, columns, reference, ref_columns=None, on_delete=None): raise NotImplementedError - def drop_fk(self, column_name, table=None): + def drop_fk(self, columns, ref_columns=None, table=None): raise NotImplementedError def not_null_action(self, column_name, action='add'): diff --git a/trytond/trytond/model/modelsql.py b/trytond/trytond/model/modelsql.py index f7a3f6480a..6940014d15 100644 --- a/trytond/trytond/model/modelsql.py +++ b/trytond/trytond/model/modelsql.py @@ -454,7 +454,7 @@ class ModelSQL(ModelStorage): # migration from 3.6 table.drop_fk(field_name) elif ref: - table.add_fk(field_name, ref, field.ondelete) + table.add_fk(field_name, ref, on_delete=field.ondelete) required = field.required # Do not set 'NOT NULL' for Binary field as the database column