def delete_model(self, model): """ Drop the model's table in the database along with any unique constraints or indexes it has. :type model: :class:`~django.db.migrations.operations.models.ModelOperation` :param model: A model for creating a table. """ # Spanner requires dropping all of a table's indexes before dropping # the table. index_names = self._constraint_names(model, index=True, primary_key=False) for index_name in index_names: trace_attributes = { "model_name": self.quote_name(model._meta.db_table), "index_name": index_name, } with trace_call( "CloudSpannerDjango.delete_model.delete_index", self.connection, trace_attributes, ): self.execute(self._delete_index_sql(model, index_name)) trace_attributes = { "model_name": self.quote_name(model._meta.db_table) } with trace_call( "CloudSpannerDjango.delete_model", self.connection, trace_attributes, ): super().delete_model(model)
def test_trace_call(self): extra_attributes = { "attribute1": "value1", # Since our database is mocked, we have to override the db.instance parameter so it is a string "db.instance": "database_name", } expected_attributes = { "db.type": "spanner", "db.engine": "django_spanner", "db.project": PROJECT, "db.instance": INSTANCE_ID, "db.name": DATABASE_ID, } expected_attributes.update(extra_attributes) with _opentelemetry_tracing.trace_call("CloudSpannerDjango.Test", _make_connection(), extra_attributes) as span: span.set_attribute("after_setup_attribute", 1) expected_attributes["after_setup_attribute"] = 1 span_list = self.ot_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(span.attributes, expected_attributes) self.assertEqual(span.name, "CloudSpannerDjango.Test") self.assertEqual(span.status.status_code, StatusCode.OK)
def test_trace_error(self): extra_attributes = {"db.instance": "database_name"} expected_attributes = { "db.type": "spanner", "db.engine": "django_spanner", "db.project": os.environ["GOOGLE_CLOUD_PROJECT"], "db.instance": "instance_id", "db.name": "database_id", } expected_attributes.update(extra_attributes) with self.assertRaises(GoogleAPICallError): with _opentelemetry_tracing.trace_call( "CloudSpannerDjango.Test", _make_connection(), extra_attributes, ) as span: from google.api_core.exceptions import InvalidArgument raise _make_rpc_error(InvalidArgument) span_list = self.ot_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertEqual(span.kind, trace_api.SpanKind.CLIENT) self.assertEqual(dict(span.attributes), expected_attributes) self.assertEqual(span.name, "CloudSpannerDjango.Test") self.assertEqual(span.status.status_code, StatusCode.ERROR)
def add_index(self, model, index): """Add index to model's table. :type model: :class:`~django.db.migrations.operations.models.ModelOperation` :param model: A model for creating a table. :type index: :class:`~django.db.migrations.operations.models.Index` :param index: An index to add. """ # Work around a bug in Django where a space isn't inserting before # DESC: https://code.djangoproject.com/ticket/30961 # This method can be removed in Django 3.1. index.fields_orders = [ (field_name, " DESC" if order == "DESC" else "") for field_name, order in index.fields_orders ] trace_attributes = { "model_name": self.quote_name(model._meta.db_table), "index": "|".join(index.fields), } with trace_call( "CloudSpannerDjango.add_index", self.connection, trace_attributes, ): super().add_index(model, index)
def remove_field(self, model, field): """ Remove the column(s) representing the field from the model's table, along with any unique constraints, foreign key constraints, or indexes caused by that field. If the field is a ManyToManyField without a value for through, it will remove the table created to track the relationship. If through is provided, it is a no-op. :type model: :class:`~django.db.migrations.operations.models.ModelOperation` :param model: A model for creating a table. :type field: :class:`~django.db.migrations.operations.models.fields.FieldOperation` :param field: The field of the table. """ # Spanner requires dropping a column's indexes before dropping the # column. index_names = self._constraint_names(model, [field.column], index=True) for index_name in index_names: trace_attributes = { "model_name": self.quote_name(model._meta.db_table), "field": field.column, "index_name": index_name, } with trace_call( "CloudSpannerDjango.remove_field.delete_index", self.connection, trace_attributes, ): self.execute(self._delete_index_sql(model, index_name)) trace_attributes = { "model_name": self.quote_name(model._meta.db_table), "field": field.column, } with trace_call( "CloudSpannerDjango.remove_field", self.connection, trace_attributes, ): super().remove_field(model, field)
def create_model(self, model): """ Create a table and any accompanying indexes or unique constraints for the given `model`. :type model: :class:`~django.db.migrations.operations.models.ModelOperation` :param model: A model for creating a table. """ # Create column SQL, add FK deferreds if needed column_sqls = [] params = [] for field in model._meta.local_fields: # SQL definition, extra_params = self.column_sql(model, field) if definition is None: continue # Check constraints can go on the column SQL here db_params = field.db_parameters(connection=self.connection) if db_params["check"]: definition += (", CONSTRAINT constraint_%s_%s_%s " % ( model._meta.db_table, self.quote_name(field.name), uuid.uuid4().hex[:6].lower(), ) + self.sql_check_constraint % db_params) # Autoincrement SQL (for backends with inline variant) col_type_suffix = field.db_type_suffix(connection=self.connection) if col_type_suffix: definition += " %s" % col_type_suffix params.extend(extra_params) # FK if field.remote_field and field.db_constraint: from_table = field.model._meta.db_table from_column = field.column to_table = field.remote_field.model._meta.db_table to_column = field.remote_field.model._meta.get_field( field.remote_field.field_name).column if self.sql_create_inline_fk: definition += ", " + self.sql_create_inline_fk % { "from_table": from_table, "from_column": from_column, "to_table": to_table, "to_column": to_column, "from_column_norm": self.quote_name(from_column), "to_table_norm": self.quote_name(to_table), "to_column_norm": self.quote_name(to_column), } elif self.connection.features.supports_foreign_keys: self.deferred_sql.append( self._create_fk_sql(model, field, "_fk_%(to_table)s_%(to_column)s")) # Add the SQL to our big list column_sqls.append("%s %s" % (self.quote_name(field.column), definition)) # Create a unique constraint separately because Spanner doesn't # allow them inline on a column. if field.unique and not field.primary_key: self.deferred_sql.append( self._create_unique_sql(model, [field.column])) # Add any unique_togethers (always deferred, as some fields might be # created afterwards, like geometry fields with some backends) for fields in model._meta.unique_together: columns = [model._meta.get_field(field).column for field in fields] self.deferred_sql.append(self._create_unique_sql(model, columns)) constraints = [ constraint.constraint_sql(model, self) for constraint in model._meta.constraints ] # Make the table sql = self.sql_create_table % { "table": self.quote_name(model._meta.db_table), "definition": ", ".join(constraint for constraint in (*column_sqls, *constraints) if constraint), "primary_key": self.quote_name(model._meta.pk.column), } if model._meta.db_tablespace: tablespace_sql = self.connection.ops.tablespace_sql( model._meta.db_tablespace) if tablespace_sql: sql += " " + tablespace_sql # Prevent using [] as params, in the case a literal '%' is used in the # definition trace_attributes = { "model_name": self.quote_name(model._meta.db_table) } with trace_call( "CloudSpannerDjango.create_model", self.connection, trace_attributes, ): self.execute(sql, params or None) # Add any field index and index_together's (deferred as SQLite # _remake_table needs it) self.deferred_sql.extend(self._model_indexes_sql(model)) # Make M2M tables for field in model._meta.local_many_to_many: if field.remote_field.through._meta.auto_created: self.create_model(field.remote_field.through)
def _alter_field( self, model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict=False, ): # Spanner requires dropping indexes before changing the nullability # of a column. nullability_changed = old_field.null != new_field.null if nullability_changed: index_names = self._constraint_names(model, [old_field.column], index=True) if index_names and not old_field.db_index: raise NotSupportedError( "Changing nullability of a field with an index other than " "Field(db_index=True) isn't yet supported.") if len(index_names) > 1: raise NotSupportedError( "Changing nullability of a field with more than one " "index isn't yet supported.") for index_name in index_names: trace_attributes = { "model_name": self.quote_name(model._meta.db_table), "alter_field": old_field.column, "index_name": index_name, } with trace_call( "CloudSpannerDjango.alter_field.delete_index", self.connection, trace_attributes, ): self.execute(self._delete_index_sql(model, index_name)) trace_attributes = { "model_name": self.quote_name(model._meta.db_table), "alter_field": old_field.column, } with trace_call( "CloudSpannerDjango.alter_field", self.connection, trace_attributes, ): super()._alter_field( model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict=False, ) # Recreate the index that was dropped earlier. if nullability_changed and new_field.db_index: trace_attributes = { "model_name": self.quote_name(model._meta.db_table), "alter_field": new_field.column, } with trace_call( "CloudSpannerDjango.alter_field.recreate_index", self.connection, trace_attributes, ): self.execute(self._create_index_sql(model, fields=[new_field]))
def test_no_trace_call(self): with _opentelemetry_tracing.trace_call( "Test", _make_connection()) as no_span: self.assertIsNone(no_span)