@@ -4044,6 +4044,136 @@ def test_upgrade_error_is_logged_not_raised(self):
40444044 # Should not raise
40454045 plugin ._ensure_schema_exists ()
40464046
4047+ def test_upgrade_preserves_existing_columns (self ):
4048+ """Existing columns are never dropped or altered during upgrade."""
4049+ plugin = self ._make_plugin (auto_schema_upgrade = True )
4050+ # Simulate a table with a subset of canonical columns plus a
4051+ # user-added custom column that is NOT in the canonical schema.
4052+ custom_field = bigquery .SchemaField ("my_custom_col" , "STRING" )
4053+ existing = mock .MagicMock (spec = bigquery .Table )
4054+ existing .schema = [
4055+ bigquery .SchemaField ("timestamp" , "TIMESTAMP" ),
4056+ bigquery .SchemaField ("event_type" , "STRING" ),
4057+ custom_field ,
4058+ ]
4059+ existing .labels = {}
4060+ plugin .client .get_table .return_value = existing
4061+ plugin ._ensure_schema_exists ()
4062+
4063+ updated_table = plugin .client .update_table .call_args [0 ][0 ]
4064+ updated_names = [f .name for f in updated_table .schema ]
4065+ # Original columns are still present and in original order.
4066+ assert updated_names [0 ] == "timestamp"
4067+ assert updated_names [1 ] == "event_type"
4068+ assert updated_names [2 ] == "my_custom_col"
4069+ # New canonical columns were appended after existing ones.
4070+ assert "agent" in updated_names
4071+ assert "content" in updated_names
4072+
4073+ def test_upgrade_from_no_label_treats_as_outdated (self ):
4074+ """A table with no version label is treated as needing upgrade."""
4075+ plugin = self ._make_plugin (auto_schema_upgrade = True )
4076+ existing = mock .MagicMock (spec = bigquery .Table )
4077+ existing .schema = list (plugin ._schema ) # All columns present
4078+ existing .labels = {} # No version label
4079+ plugin .client .get_table .return_value = existing
4080+ plugin ._ensure_schema_exists ()
4081+
4082+ # update_table should be called to stamp the version label even
4083+ # though no new columns were needed.
4084+ plugin .client .update_table .assert_called_once ()
4085+ updated_table = plugin .client .update_table .call_args [0 ][0 ]
4086+ assert (
4087+ updated_table .labels [
4088+ bigquery_agent_analytics_plugin ._SCHEMA_VERSION_LABEL_KEY
4089+ ]
4090+ == bigquery_agent_analytics_plugin ._SCHEMA_VERSION
4091+ )
4092+
4093+ def test_upgrade_from_older_version_label (self ):
4094+ """A table with an older version label triggers upgrade."""
4095+ plugin = self ._make_plugin (auto_schema_upgrade = True )
4096+ existing = mock .MagicMock (spec = bigquery .Table )
4097+ existing .schema = [
4098+ bigquery .SchemaField ("timestamp" , "TIMESTAMP" ),
4099+ bigquery .SchemaField ("event_type" , "STRING" ),
4100+ ]
4101+ # Simulate a table stamped with an older version.
4102+ existing .labels = {
4103+ bigquery_agent_analytics_plugin ._SCHEMA_VERSION_LABEL_KEY : "0" ,
4104+ }
4105+ plugin .client .get_table .return_value = existing
4106+ plugin ._ensure_schema_exists ()
4107+
4108+ plugin .client .update_table .assert_called_once ()
4109+ updated_table = plugin .client .update_table .call_args [0 ][0 ]
4110+ # Version label should be updated to current.
4111+ assert (
4112+ updated_table .labels [
4113+ bigquery_agent_analytics_plugin ._SCHEMA_VERSION_LABEL_KEY
4114+ ]
4115+ == bigquery_agent_analytics_plugin ._SCHEMA_VERSION
4116+ )
4117+ # Missing columns should have been added.
4118+ updated_names = {f .name for f in updated_table .schema }
4119+ assert "agent" in updated_names
4120+ assert "content" in updated_names
4121+
4122+ def test_upgrade_is_idempotent (self ):
4123+ """Calling _ensure_schema_exists twice doesn't double-update."""
4124+ plugin = self ._make_plugin (auto_schema_upgrade = True )
4125+
4126+ # First call: table exists with old schema.
4127+ existing = mock .MagicMock (spec = bigquery .Table )
4128+ existing .schema = [
4129+ bigquery .SchemaField ("timestamp" , "TIMESTAMP" ),
4130+ ]
4131+ existing .labels = {}
4132+ plugin .client .get_table .return_value = existing
4133+ plugin ._ensure_schema_exists ()
4134+ assert plugin .client .update_table .call_count == 1
4135+
4136+ # Second call: table now has current version label.
4137+ existing .labels = {
4138+ bigquery_agent_analytics_plugin ._SCHEMA_VERSION_LABEL_KEY : (
4139+ bigquery_agent_analytics_plugin ._SCHEMA_VERSION
4140+ ),
4141+ }
4142+ plugin .client .update_table .reset_mock ()
4143+ plugin ._ensure_schema_exists ()
4144+ plugin .client .update_table .assert_not_called ()
4145+
4146+ def test_update_table_receives_schema_and_labels_fields (self ):
4147+ """update_table is called with update_fields=['schema', 'labels']."""
4148+ plugin = self ._make_plugin (auto_schema_upgrade = True )
4149+ existing = mock .MagicMock (spec = bigquery .Table )
4150+ existing .schema = [
4151+ bigquery .SchemaField ("timestamp" , "TIMESTAMP" ),
4152+ ]
4153+ existing .labels = {}
4154+ plugin .client .get_table .return_value = existing
4155+ plugin ._ensure_schema_exists ()
4156+
4157+ call_args = plugin .client .update_table .call_args
4158+ update_fields = call_args [0 ][1 ]
4159+ assert "schema" in update_fields
4160+ assert "labels" in update_fields
4161+
4162+ def test_auto_schema_upgrade_defaults_to_true (self ):
4163+ """Default config has auto_schema_upgrade enabled."""
4164+ config = bigquery_agent_analytics_plugin .BigQueryLoggerConfig ()
4165+ assert config .auto_schema_upgrade is True
4166+
4167+ def test_create_table_conflict_is_ignored (self ):
4168+ """Race condition (Conflict) during create_table is silently handled."""
4169+ plugin = self ._make_plugin ()
4170+ plugin .client .get_table .side_effect = cloud_exceptions .NotFound ("not found" )
4171+ plugin .client .create_table .side_effect = cloud_exceptions .Conflict (
4172+ "already exists"
4173+ )
4174+ # Should not raise.
4175+ plugin ._ensure_schema_exists ()
4176+
40474177
40484178class TestToolProvenance :
40494179 """Tests for _get_tool_origin helper."""
0 commit comments