diff --git a/spinta/backends/postgresql/types/array/init.py b/spinta/backends/postgresql/types/array/init.py index 9dde1559..4a4c35a6 100644 --- a/spinta/backends/postgresql/types/array/init.py +++ b/spinta/backends/postgresql/types/array/init.py @@ -15,40 +15,39 @@ def prepare(context: Context, backend: PostgreSQL, dtype: Array, **kwargs): prop = dtype.prop - if dtype.model is None: - columns = commands.prepare(context, backend, dtype.items, **kwargs) - - assert columns is not None - if not isinstance(columns, list): - columns = [columns] - pkey_type = commands.get_primary_key_type(context, backend) - # TODO: When all list items will have unique id, also add reference to - # parent list id. - # if prop.list: - # parent_list_table_name = get_pg_name( - # get_table_name(prop.list, TableType.LIST) - # ) - # columns += [ - # # Parent list table id. - # sa.Column('_lid', pkey_type, sa.ForeignKey( - # f'{parent_list_table_name}._id', ondelete='CASCADE', - # )), - # ] - name = get_pg_name(get_table_name(prop, TableType.LIST)) - main_table_name = get_pg_name(get_table_name(prop.model)) - table = sa.Table( - name, backend.schema, - # TODO: List tables eventually will have _id in order to uniquelly - # identify list item. - # sa.Column('_id', pkey_type, primary_key=True), - sa.Column('_txn', pkey_type, index=True), - sa.Column('_rid', pkey_type, sa.ForeignKey( - f'{main_table_name}._id', ondelete='CASCADE', - )), - - *columns, - ) - backend.add_table(table, prop, TableType.LIST) + columns = commands.prepare(context, backend, dtype.items, **kwargs) + + assert columns is not None + if not isinstance(columns, list): + columns = [columns] + pkey_type = commands.get_primary_key_type(context, backend) + # TODO: When all list items will have unique id, also add reference to + # parent list id. + # if prop.list: + # parent_list_table_name = get_pg_name( + # get_table_name(prop.list, TableType.LIST) + # ) + # columns += [ + # # Parent list table id. + # sa.Column('_lid', pkey_type, sa.ForeignKey( + # f'{parent_list_table_name}._id', ondelete='CASCADE', + # )), + # ] + name = get_pg_name(get_table_name(prop, TableType.LIST)) + main_table_name = get_pg_name(get_table_name(prop.model)) + table = sa.Table( + name, backend.schema, + # TODO: List tables eventually will have _id in order to uniquelly + # identify list item. + # sa.Column('_id', pkey_type, primary_key=True), + sa.Column('_txn', pkey_type, index=True), + sa.Column('_rid', pkey_type, sa.ForeignKey( + f'{main_table_name}._id', ondelete='CASCADE', + )), + + *columns, + ) + backend.add_table(table, prop, TableType.LIST) if prop.list is None: # For fast whole resource access we also store whole list in a JSONB. diff --git a/spinta/exceptions.py b/spinta/exceptions.py index 307ab3f9..cfa08aee 100644 --- a/spinta/exceptions.py +++ b/spinta/exceptions.py @@ -1002,3 +1002,53 @@ class InvalidClientFileFormat(UserError): class MissingRefModel(UserError): template = 'Property "{property_name}" of type "{property_type}" in the model "{model_name}" should have a model name in the `ref` column, to which it refers.' + + +class UnableToMapIntermediateTable(UserError): + template = 'Unable to map intermediate table to `Array` property.' + + +class InvalidIntermediateTableMappingRefCount(UserError): + template = 'Intermediate table can only be mapped with 2 properties (left and right), but were given {ref_count}.' + + +class SameModelIntermediateTableMapping(UserError): + template = ''' + Intermediate table is referencing same model, need to explicitly give the mapping order. + Set it as intermediate table's model primary key, or by explicitly setting reference properties. + First is left property (source), second is right property (targeted value). + ''' + + +class IntermediateTableMappingInvalidType(UserError): + template = ''' + Intermediate table requires `Ref` type columns for it's mapping. + {property_name!r} property is of type {property_type!r}. + ''' + + +class IntermediateTableValueTypeMissmatch(UserError): + template = ''' + Array item type needs to match intermediate table's right property type. + Given array type: {array_type!r}, intermediate table's type: {intermediate_type!r}. + ''' + + +class IntermediateTableRefModelMissmatch(UserError): + template = ''' + Intermediate table's left property `Ref` model needs to be the same as array's model. + Array's model: {array_model!r}, intermediate table's left property `Ref` model: {left_model!r}. + ''' + + +class IntermediateTableRefPropertyModelMissmatch(UserError): + template = ''' + Array's `Ref` property model does not match intermediate table's right `Ref` property model. + Array's `Ref` model: {array_ref_model!r}, intermediate table's right property `Ref` model: {right_model!r}. + ''' + + +class IntermediateTableMissingMappingProperty(UserError): + template = ''' + Intermediate table's {side} property cannot be None. + ''' diff --git a/spinta/types/array/check.py b/spinta/types/array/check.py index 2e35b258..f1c5edb0 100644 --- a/spinta/types/array/check.py +++ b/spinta/types/array/check.py @@ -1,5 +1,8 @@ from spinta import commands from spinta.components import Context +from spinta.exceptions import IntermediateTableMappingInvalidType, IntermediateTableValueTypeMissmatch, \ + IntermediateTableRefModelMissmatch, IntermediateTableRefPropertyModelMissmatch, \ + IntermediateTableMissingMappingProperty from spinta.types.datatype import Array, Ref @@ -10,22 +13,42 @@ def check(context: Context, dtype: Array): if dtype.model is not None: if dtype.left_prop is None: - raise Exception("IF INTERMEDIATE MODEL GIVEN, LEFT PROP IS MUST") + raise IntermediateTableMissingMappingProperty(dtype, side="left") if dtype.right_prop is None: - raise Exception("IF INTERMEDIATE MODEL GIVEN, RIGHT PROP IS MUST") + raise IntermediateTableMissingMappingProperty(dtype, side="right") if not isinstance(dtype.left_prop.dtype, Ref): - raise Exception("INTERMEDIATE MODEL MAPPING PROP NEEDS TO BE REF TYPE") + raise IntermediateTableMappingInvalidType( + dtype, + prop_name=dtype.left_prop.name, + prop_type=dtype.left_prop.dtype.name + ) if not isinstance(dtype.right_prop.dtype, Ref): - raise Exception("INTERMEDIATE MODEL MAPPING PROP NEEDS TO BE REF TYPE") - - if dtype.items is not None and not isinstance(dtype.items.dtype, Ref): - raise Exception("INTERMEDIATE MODEL REQUIRES REF TYPE TO BE MAPPED FOR RETURN") + raise IntermediateTableMappingInvalidType( + dtype, + prop_name=dtype.right_prop.name, + prop_type=dtype.right_prop.dtype.name + ) + + if dtype.items is not None and not isinstance(dtype.items.dtype, type(dtype.right_prop.dtype)): + raise IntermediateTableValueTypeMissmatch( + dtype, + array_type=dtype.items.dtype.name, + intermediate_type=dtype.right_prop.dtype.name + ) if dtype.left_prop.dtype.model != dtype.prop.model: - raise Exception("INTERMEDIATE MODEL LEFT PROP MODEL HAS TO BE SAME AS SOURCE") + raise IntermediateTableRefModelMissmatch( + dtype, + array_model=dtype.prop.model.name, + left_model=dtype.left_prop.dtype.model.name + ) if dtype.items.dtype.model != dtype.right_prop.dtype.model: - raise Exception("INTERMEDIATE MODEL RIGHT PROP DOES NOT MATCH REF MODEL WITH ITEM") + raise IntermediateTableRefPropertyModelMissmatch( + dtype, + array_ref_model=dtype.items.dtype.model.name, + right_model=dtype.right_prop.dtype.model.name + ) diff --git a/spinta/types/array/link.py b/spinta/types/array/link.py index 3de7d7a8..f96630fc 100644 --- a/spinta/types/array/link.py +++ b/spinta/types/array/link.py @@ -4,7 +4,8 @@ from spinta import commands from spinta.components import Context, Model, Property -from spinta.exceptions import ModelReferenceNotFound, ModelReferenceKeyNotFound +from spinta.exceptions import ModelReferenceNotFound, ModelReferenceKeyNotFound, \ + InvalidIntermediateTableMappingRefCount, UnableToMapIntermediateTable, SameModelIntermediateTableMapping from spinta.types.datatype import Array, PartialArray, ArrayBackRef, Ref, DataType from spinta.types.helpers import set_dtype_backend @@ -54,7 +55,7 @@ def _compare_models(source: Model, target: str | Model) -> bool: def _extract_intermediate_table_properties(source: Array, intermediate_model: Model) -> (Property, Property): if source.refprops: if len(source.refprops) != 2: - raise Exception("EXPOSED INTERMEDIATE ARRAY TABLE REQUIRES 2 REF PROPS (LEFT, RIGHT)") + raise InvalidIntermediateTableMappingRefCount(source, ref_count=len(source.refprops)) return source.refprops @@ -67,10 +68,10 @@ def _extract_intermediate_table_properties(source: Array, intermediate_model: Mo ref_properties = [prop for prop in intermediate_model.flatprops.values() if isinstance(prop.dtype, Ref)] if len(ref_properties) != 2: - raise Exception("COULD NOT MAP INTERMEDIATE ARRAY TABLE") + raise UnableToMapIntermediateTable(source) if all(_compare_models(source.prop.model, prop.dtype.model) for prop in ref_properties): - raise Exception("SAME MODELS BEING REFERENCED, CANNOT DETERMINE WHICH ONE IS LEFT AND WHICH ONE IS RIGHT") + raise SameModelIntermediateTableMapping(source) return ref_properties