diff --git a/src/BigQuery/Dataset.php b/src/BigQuery/Dataset.php index 87d217650a1a..e23138e0fd35 100644 --- a/src/BigQuery/Dataset.php +++ b/src/BigQuery/Dataset.php @@ -19,6 +19,7 @@ use Google\Cloud\BigQuery\Connection\ConnectionInterface; use Google\Cloud\Core\ArrayTrait; +use Google\Cloud\Core\ConcurrencyControlTrait; use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Core\Iterator\ItemIterator; use Google\Cloud\Core\Iterator\PageIterator; @@ -30,6 +31,7 @@ class Dataset { use ArrayTrait; + use ConcurrencyControlTrait; /** * @var ConnectionInterface Represents a connection to BigQuery. @@ -121,6 +123,11 @@ public function delete(array $options = []) /** * Update the dataset. * + * Providing an `etag` key as part of `$metadata` will enable simultaneous + * update protection. This is useful in preventing override of modifications + * made by another user. The resource's current etag can be obtained via a + * GET request on the resource. + * * Example: * ``` * $dataset->update([ @@ -129,6 +136,7 @@ public function delete(array $options = []) * ``` * * @see https://cloud.google.com/bigquery/docs/reference/v2/datasets/patch Datasets patch API documentation. + * @see https://cloud.google.com/bigquery/docs/api-performance#patch Patch (Partial Update) * * @param array $metadata The available options for metadata are outlined * at the [Dataset Resource API docs](https://cloud.google.com/bigquery/docs/reference/v2/datasets#resource) @@ -137,7 +145,9 @@ public function delete(array $options = []) public function update(array $metadata, array $options = []) { $options += $metadata; - $this->info = $this->connection->patchDataset($options + $this->identity); + $this->info = $this->connection->patchDataset( + $this->applyEtagHeader($options + $this->identity) + ); return $this->info; } diff --git a/src/BigQuery/Table.php b/src/BigQuery/Table.php index ebb29efa9dbc..12e8c7de2687 100644 --- a/src/BigQuery/Table.php +++ b/src/BigQuery/Table.php @@ -19,6 +19,7 @@ use Google\Cloud\BigQuery\Connection\ConnectionInterface; use Google\Cloud\Core\ArrayTrait; +use Google\Cloud\Core\ConcurrencyControlTrait; use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Core\Iterator\ItemIterator; use Google\Cloud\Core\Iterator\PageIterator; @@ -33,6 +34,7 @@ class Table { use ArrayTrait; + use ConcurrencyControlTrait; use JobConfigurationTrait; /** @@ -125,6 +127,11 @@ public function delete(array $options = []) /** * Update the table. * + * Providing an `etag` key as part of `$metadata` will enable simultaneous + * update protection. This is useful in preventing override of modifications + * made by another user. The resource's current etag can be obtained via a + * GET request on the resource. + * * Example: * ``` * $table->update([ @@ -133,6 +140,7 @@ public function delete(array $options = []) * ``` * * @see https://cloud.google.com/bigquery/docs/reference/v2/tables/patch Tables patch API documentation. + * @see https://cloud.google.com/bigquery/docs/api-performance#patch Patch (Partial Update) * * @param array $metadata The available options for metadata are outlined * at the [Table Resource API docs](https://cloud.google.com/bigquery/docs/reference/v2/tables#resource) @@ -141,7 +149,9 @@ public function delete(array $options = []) public function update(array $metadata, array $options = []) { $options += $metadata; - $this->info = $this->connection->patchTable($options + $this->identity); + $this->info = $this->connection->patchTable( + $this->applyEtagHeader($options + $this->identity) + ); return $this->info; } diff --git a/src/Core/ConcurrencyControlTrait.php b/src/Core/ConcurrencyControlTrait.php new file mode 100644 index 000000000000..aca1f9e7ead4 --- /dev/null +++ b/src/Core/ConcurrencyControlTrait.php @@ -0,0 +1,48 @@ +assertEquals($metadata['friendlyName'], $info['friendlyName']); } + /** + * @expectedException Google\Cloud\Core\Exception\FailedPreconditionException + */ + public function testUpdateDatasetConcurrentUpdateFails() + { + $data = [ + 'friendlyName' => 'foo', + 'etag' => 'blah' + ]; + + self::$dataset->update($data); + } + public function testReloadsDataset() { $this->assertEquals('bigquery#dataset', self::$dataset->reload()['kind']); diff --git a/tests/system/BigQuery/ManageTablesTest.php b/tests/system/BigQuery/ManageTablesTest.php index 5d9a40adfd6b..bf4e7bda08b7 100644 --- a/tests/system/BigQuery/ManageTablesTest.php +++ b/tests/system/BigQuery/ManageTablesTest.php @@ -129,6 +129,19 @@ public function testUpdateTable() $this->assertEquals($metadata['friendlyName'], $info['friendlyName']); } + /** + * @expectedException Google\Cloud\Core\Exception\FailedPreconditionException + */ + public function testUpdateTableConcurrentUpdateFails() + { + $data = [ + 'friendlyName' => 'foo', + 'etag' => 'blah' + ]; + + self::$table->update($data); + } + public function testReloadsTable() { $this->assertEquals('bigquery#table', self::$table->reload()['kind']); diff --git a/tests/unit/BigQuery/DatasetTest.php b/tests/unit/BigQuery/DatasetTest.php index 1f9703811f96..678fa95b6109 100644 --- a/tests/unit/BigQuery/DatasetTest.php +++ b/tests/unit/BigQuery/DatasetTest.php @@ -94,6 +94,22 @@ public function testUpdatesData() $this->assertEquals($updateData['friendlyName'], $dataset->info()['friendlyName']); } + public function testUpdatesDataWithEtag() + { + $updateData = ['friendlyName' => 'wow a name', 'etag' => 'foo']; + $this->connection->patchDataset(Argument::that(function ($args) { + if ($args['restOptions']['headers']['If-Match'] !== 'foo') return false; + + return true; + })) + ->willReturn($updateData) + ->shouldBeCalledTimes(1); + $dataset = $this->getDataset($this->connection, ['friendlyName' => 'another name']); + $dataset->update($updateData); + + $this->assertEquals($updateData['friendlyName'], $dataset->info()['friendlyName']); + } + public function testGetsTable() { $dataset = $this->getDataset($this->connection); diff --git a/tests/unit/BigQuery/TableTest.php b/tests/unit/BigQuery/TableTest.php index 7082d2cc8466..783c5d65dfd5 100644 --- a/tests/unit/BigQuery/TableTest.php +++ b/tests/unit/BigQuery/TableTest.php @@ -121,6 +121,22 @@ public function testDelete() } public function testUpdatesData() + { + $updateData = ['friendlyName' => 'wow a name', 'etag' => 'foo']; + $this->connection->patchTable(Argument::that(function ($args) { + if ($args['restOptions']['headers']['If-Match'] !== 'foo') return false; + + return true; + })) + ->willReturn($updateData) + ->shouldBeCalledTimes(1); + $table = $this->getTable($this->connection, ['friendlyName' => 'another name']); + $table->update($updateData); + + $this->assertEquals($updateData['friendlyName'], $table->info()['friendlyName']); + } + + public function testUpdatesDataWithEtag() { $updateData = ['friendlyName' => 'wow a name']; $this->connection->patchTable(Argument::any()) diff --git a/tests/unit/Core/ConcurrencyControlTraitTest.php b/tests/unit/Core/ConcurrencyControlTraitTest.php new file mode 100644 index 000000000000..3e1bc0e1fe74 --- /dev/null +++ b/tests/unit/Core/ConcurrencyControlTraitTest.php @@ -0,0 +1,53 @@ +trait = \Google\Cloud\Dev\impl(ConcurrencyControlTrait::class); + } + + public function testApplyEtagHeader() + { + $input = ['etag' => self::ETAG]; + + $res = $this->trait->call('applyEtagHeader', [$input]); + + $this->assertEquals(self::ETAG, $res['restOptions']['headers']['If-Match']); + } + + public function testApplyEtagHeaderCustomName() + { + $input = ['test' => self::ETAG]; + + $res = $this->trait->call('applyEtagHeader', [$input, 'test']); + + $this->assertEquals(self::ETAG, $res['restOptions']['headers']['If-Match']); + } +}