diff --git a/samples/samples/conftest.py b/samples/samples/conftest.py index 9108a5892a..f4d21c6926 100644 --- a/samples/samples/conftest.py +++ b/samples/samples/conftest.py @@ -74,6 +74,12 @@ def instance_id(): return f"test-instance-{uuid.uuid4().hex[:10]}" +@pytest.fixture(scope="module") +def multi_region_instance_id(): + """Unique id for the multi-region instance used in samples.""" + return f"multi-instance-{uuid.uuid4().hex[:10]}" + + @pytest.fixture(scope="module") def instance_config(spanner_client): return "{}/instanceConfigs/{}".format( @@ -81,6 +87,13 @@ def instance_config(spanner_client): ) +@pytest.fixture(scope="module") +def multi_region_instance_config(spanner_client): + return "{}/instanceConfigs/{}".format( + spanner_client.project_name, "nam3" + ) + + @pytest.fixture(scope="module") def sample_instance( spanner_client, cleanup_old_instances, instance_id, instance_config, sample_name, @@ -113,6 +126,42 @@ def sample_instance( sample_instance.delete() +@pytest.fixture(scope="module") +def multi_region_instance( + spanner_client, + cleanup_old_instances, + multi_region_instance_id, + multi_region_instance_config, + sample_name, +): + multi_region_instance = spanner_client.instance( + multi_region_instance_id, + multi_region_instance_config, + labels={ + "cloud_spanner_samples": "true", + "sample_name": sample_name, + "created": str(int(time.time())) + }, + ) + retry_429 = retry.RetryErrors(exceptions.ResourceExhausted, delay=15) + op = retry_429(multi_region_instance.create)() + op.result(120) # block until completion + + # Eventual consistency check + retry_found = retry.RetryResult(bool) + retry_found(multi_region_instance.exists)() + + yield multi_region_instance + + for database_pb in multi_region_instance.list_databases(): + database.Database.from_pb(database_pb, multi_region_instance).drop() + + for backup_pb in multi_region_instance.list_backups(): + backup.Backup.from_pb(backup_pb, multi_region_instance).delete() + + multi_region_instance.delete() + + @pytest.fixture(scope="module") def database_id(): """Id for the database used in samples. diff --git a/samples/samples/snippets.py b/samples/samples/snippets.py index c6c3972e32..0cc68856ea 100644 --- a/samples/samples/snippets.py +++ b/samples/samples/snippets.py @@ -96,6 +96,53 @@ def create_instance_with_processing_units(instance_id, processing_units): # [END spanner_create_instance_with_processing_units] +# [START spanner_get_instance_config] +def get_instance_config(instance_config): + """Gets the leader options for the instance configuration.""" + spanner_client = spanner.Client() + config_name = "{}/instanceConfigs/{}".format(spanner_client.project_name, instance_config) + config = spanner_client.instance_admin_api.get_instance_config(name=config_name) + print("Available leader options for instance config {}: {}".format( + instance_config, config.leader_options)) + + +# [END spanner_get_instance_config] + + +# [START spanner_list_instance_configs] +def list_instance_config(): + """Lists the available instance configurations.""" + spanner_client = spanner.Client() + configs = spanner_client.list_instance_configs() + for config in configs: + print( + "Available leader options for instance config {}: {}".format( + config.name, config.leader_options + ) + ) + + +# [END spanner_list_instance_configs] + + +# [START spanner_list_databases] +def list_databases(instance_id): + """Lists databases and their leader options.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + databases = list(instance.list_databases()) + for database in databases: + print( + "Database {} has default leader {}".format( + database.name, database.default_leader + ) + ) + + +# [END spanner_list_databases] + + # [START spanner_create_database] def create_database(instance_id, database_id): """Creates a database and tables for sample data.""" @@ -168,6 +215,112 @@ def create_database_with_encryption_key(instance_id, database_id, kms_key_name): # [END spanner_create_database_with_encryption_key] +# [START spanner_create_database_with_default_leader] +def create_database_with_default_leader( + instance_id, database_id, default_leader +): + """Creates a database with tables with a default leader.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + database = instance.database( + database_id, + ddl_statements=[ + """CREATE TABLE Singers ( + SingerId INT64 NOT NULL, + FirstName STRING(1024), + LastName STRING(1024), + SingerInfo BYTES(MAX) + ) PRIMARY KEY (SingerId)""", + """CREATE TABLE Albums ( + SingerId INT64 NOT NULL, + AlbumId INT64 NOT NULL, + AlbumTitle STRING(MAX) + ) PRIMARY KEY (SingerId, AlbumId), + INTERLEAVE IN PARENT Singers ON DELETE CASCADE""", + "ALTER DATABASE {}" + " SET OPTIONS (default_leader = '{}')".format(database_id, default_leader), + ], + ) + operation = database.create() + + print("Waiting for operation to complete...") + operation.result(120) + + database.reload() + + print( + "Database {} created with default leader {}".format( + database.name, database.default_leader + ) + ) + + +# [END spanner_create_database_with_default_leader] + + +# [START spanner_update_database_with_default_leader] +def update_database_with_default_leader( + instance_id, database_id, default_leader +): + """Updates a database with tables with a default leader.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + + database = instance.database(database_id) + + operation = database.update_ddl(["ALTER DATABASE {}" + " SET OPTIONS (default_leader = '{}')".format(database_id, default_leader)]) + operation.result(120) + + database.reload() + + print( + "Database {} updated with default leader {}".format( + database.name, database.default_leader + ) + ) + + +# [END spanner_update_database_with_default_leader] + + +# [START spanner_get_database_ddl] +def get_database_ddl(instance_id, database_id): + """Gets the database DDL statements.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + ddl = spanner_client.database_admin_api.get_database_ddl(database=database.name) + print("Retrieved database DDL for {}".format(database_id)) + for statement in ddl.statements: + print(statement) + + +# [END spanner_get_database_ddl] + + +# [START spanner_query_information_schema_database_options] +def query_information_schema_database_options(instance_id, database_id): + """Queries the default leader of a database.""" + spanner_client = spanner.Client() + instance = spanner_client.instance(instance_id) + database = instance.database(database_id) + with database.snapshot() as snapshot: + results = snapshot.execute_sql( + "SELECT OPTION_VALUE AS default_leader " + "FROM INFORMATION_SCHEMA.DATABASE_OPTIONS " + "WHERE SCHEMA_NAME = '' AND OPTION_NAME = 'default_leader'" + ) + for result in results: + print("Database {} has default leader {}".format( + database_id, result[0] + )) + + +# [END spanner_query_information_schema_database_options] + + # [START spanner_insert_data] def insert_data(instance_id, database_id): """Inserts sample data into the given database. diff --git a/samples/samples/snippets_test.py b/samples/samples/snippets_test.py index 4a8d1991d3..636b4b5e91 100644 --- a/samples/samples/snippets_test.py +++ b/samples/samples/snippets_test.py @@ -73,6 +73,11 @@ def cmek_database_id(): return f"cmek-db-{uuid.uuid4().hex[:10]}" +@pytest.fixture(scope="module") +def default_leader_database_id(): + return f"leader_db_{uuid.uuid4().hex[:10]}" + + @pytest.fixture(scope="module") def database_ddl(): """Sequence of DDL statements used to set up the database. @@ -82,6 +87,12 @@ def database_ddl(): return [CREATE_TABLE_SINGERS, CREATE_TABLE_ALBUMS] +@pytest.fixture(scope="module") +def default_leader(): + """ Default leader for multi-region instances. """ + return "us-east4" + + def test_create_instance_explicit(spanner_client, create_instance_id): # Rather than re-use 'sample_isntance', we create a new instance, to # ensure that the 'create_instance' snippet is tested. @@ -119,6 +130,59 @@ def test_create_database_with_encryption_config(capsys, instance_id, cmek_databa assert kms_key_name in out +def test_get_instance_config(capsys): + instance_config = "nam6" + snippets.get_instance_config(instance_config) + out, _ = capsys.readouterr() + assert instance_config in out + + +def test_list_instance_config(capsys): + snippets.list_instance_config() + out, _ = capsys.readouterr() + assert "regional-us-central1" in out + + +def test_list_databases(capsys, instance_id): + snippets.list_databases(instance_id) + out, _ = capsys.readouterr() + assert "has default leader" in out + + +def test_create_database_with_default_leader(capsys, multi_region_instance, multi_region_instance_id, default_leader_database_id, default_leader): + retry_429 = RetryErrors(exceptions.ResourceExhausted, delay=15) + retry_429(snippets.create_database_with_default_leader)( + multi_region_instance_id, default_leader_database_id, default_leader + ) + out, _ = capsys.readouterr() + assert default_leader_database_id in out + assert default_leader in out + + +def test_update_database_with_default_leader(capsys, multi_region_instance, multi_region_instance_id, default_leader_database_id, default_leader): + retry_429 = RetryErrors(exceptions.ResourceExhausted, delay=15) + retry_429(snippets.update_database_with_default_leader)( + multi_region_instance_id, default_leader_database_id, default_leader + ) + out, _ = capsys.readouterr() + assert default_leader_database_id in out + assert default_leader in out + + +def test_get_database_ddl(capsys, instance_id, sample_database): + snippets.get_database_ddl(instance_id, sample_database.database_id) + out, _ = capsys.readouterr() + assert sample_database.database_id in out + + +def test_query_information_schema_database_options(capsys, multi_region_instance, multi_region_instance_id, default_leader_database_id, default_leader): + snippets.query_information_schema_database_options( + multi_region_instance_id, default_leader_database_id + ) + out, _ = capsys.readouterr() + assert default_leader in out + + @pytest.mark.dependency(name="insert_data") def test_insert_data(capsys, instance_id, sample_database): snippets.insert_data(instance_id, sample_database.database_id)