Skip to content

Commit 6da206c

Browse files
Merge branch 'master' of https://github.com/googleapis/python-spanner into spanner_issue_220
2 parents 2044ce8 + 1c2a64f commit 6da206c

36 files changed

Lines changed: 1796 additions & 486 deletions

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@
44

55
[1]: https://pypi.org/project/google-cloud-spanner/#history
66

7+
## [3.1.0](https://www.github.com/googleapis/python-spanner/compare/v3.0.0...v3.1.0) (2021-02-23)
8+
9+
10+
### Features
11+
12+
* add support for Point In Time Recovery (PITR) ([#148](https://www.github.com/googleapis/python-spanner/issues/148)) ([a082e5d](https://www.github.com/googleapis/python-spanner/commit/a082e5d7d2195ab9429a8e0bef4a664b59fdf771))
13+
* add support to log commit stats ([#205](https://www.github.com/googleapis/python-spanner/issues/205)) ([434967e](https://www.github.com/googleapis/python-spanner/commit/434967e3a433b6516f5792dcbfef7ba950f091c5))
14+
15+
16+
### Bug Fixes
17+
18+
* connection attribute of connection class and include related unit tests ([#228](https://www.github.com/googleapis/python-spanner/issues/228)) ([4afea77](https://www.github.com/googleapis/python-spanner/commit/4afea77812e021859377216cd950e1d9fc965ba8))
19+
* **db_api:** add dummy lastrowid attribute ([#227](https://www.github.com/googleapis/python-spanner/issues/227)) ([0375914](https://www.github.com/googleapis/python-spanner/commit/0375914342de98e3903bae2097142325028d18d9))
20+
* fix execute insert for homogeneous statement ([#233](https://www.github.com/googleapis/python-spanner/issues/233)) ([36b12a7](https://www.github.com/googleapis/python-spanner/commit/36b12a7b53cdbedf543d2b3bb132fb9e13cefb65))
21+
* use datetime timezone info when generating timestamp strings ([#236](https://www.github.com/googleapis/python-spanner/issues/236)) ([539f145](https://www.github.com/googleapis/python-spanner/commit/539f14533afd348a328716aa511d453ca3bb19f5))
22+
23+
24+
### Performance Improvements
25+
26+
* improve streaming performance ([#240](https://www.github.com/googleapis/python-spanner/issues/240)) ([3e35d4a](https://www.github.com/googleapis/python-spanner/commit/3e35d4a0217081bcab4ee31b642cd3bff5e6f4b5))
27+
728
## [3.0.0](https://www.github.com/googleapis/python-spanner/compare/v2.1.0...v3.0.0) (2021-01-15)
829

930

docs/api-reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Most likely, you will be interacting almost exclusively with these:
1010
client-api
1111
instance-api
1212
database-api
13+
table-api
1314
session-api
1415
keyset-api
1516
snapshot-api

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Usage Documentation
1111
client-usage
1212
instance-usage
1313
database-usage
14+
table-usage
1415
batch-usage
1516
snapshot-usage
1617
transaction-usage

docs/table-api.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Table API
2+
=========
3+
4+
.. automodule:: google.cloud.spanner_v1.table
5+
:members:
6+
:show-inheritance:

docs/table-usage.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Table Admin
2+
===========
3+
4+
After creating an :class:`~google.cloud.spanner_v1.database.Database`, you can
5+
interact with individual tables for that instance.
6+
7+
8+
List Tables
9+
-----------
10+
11+
To iterate over all existing tables for an database, use its
12+
:meth:`~google.cloud.spanner_v1.database.Database.list_tables` method:
13+
14+
.. code:: python
15+
16+
for table in database.list_tables():
17+
# `table` is a `Table` object.
18+
19+
This method yields :class:`~google.cloud.spanner_v1.table.Table` objects.
20+
21+
22+
Table Factory
23+
-------------
24+
25+
A :class:`~google.cloud.spanner_v1.table.Table` object can be created with the
26+
:meth:`~google.cloud.spanner_v1.database.Database.table` factory method:
27+
28+
.. code:: python
29+
30+
table = database.table("my_table_id")
31+
if table.exists():
32+
print("Table with ID 'my_table' exists.")
33+
else:
34+
print("Table with ID 'my_table' does not exist."
35+
36+
37+
Getting the Table Schema
38+
------------------------
39+
40+
Use the :attr:`~google.cloud.spanner_v1.table.Table.schema` property to inspect
41+
the columns of a table as a list of
42+
:class:`~google.cloud.spanner_v1.types.StructType.Field` objects.
43+
44+
.. code:: python
45+
46+
for field in table.schema
47+
# `field` is a `Field` object.

google/cloud/spanner_dbapi/cursor.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def __init__(self, connection):
5656
self._itr = None
5757
self._result_set = None
5858
self._row_count = _UNSET_COUNT
59+
self.lastrowid = None
5960
self.connection = connection
6061
self._is_closed = False
6162
# the currently running SQL statement results checksum
@@ -89,7 +90,10 @@ def description(self):
8990
:rtype: tuple
9091
:returns: A tuple of columns' information.
9192
"""
92-
if not (self._result_set and self._result_set.metadata):
93+
if not self._result_set:
94+
return None
95+
96+
if not getattr(self._result_set, "metadata", None):
9397
return None
9498

9599
row_type = self._result_set.metadata.row_type
@@ -279,7 +283,7 @@ def fetchall(self):
279283
self._checksum.consume_result(row)
280284
res.append(row)
281285
except Aborted:
282-
self._connection.retry_transaction()
286+
self.connection.retry_transaction()
283287
return self.fetchall()
284288

285289
return res
@@ -310,7 +314,7 @@ def fetchmany(self, size=None):
310314
except StopIteration:
311315
break
312316
except Aborted:
313-
self._connection.retry_transaction()
317+
self.connection.retry_transaction()
314318
return self.fetchmany(size)
315319

316320
return items

google/cloud/spanner_dbapi/parse_utils.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -306,19 +306,15 @@ def parse_insert(insert_sql, params):
306306
# Case c)
307307

308308
columns = [mi.strip(" `") for mi in match.group("columns").split(",")]
309-
sql_params_list = []
310-
insert_sql_preamble = "INSERT INTO %s (%s) VALUES %s" % (
311-
match.group("table_name"),
312-
match.group("columns"),
313-
values.argv[0],
314-
)
315309
values_pyformat = [str(arg) for arg in values.argv]
316310
rows_list = rows_for_insert_or_update(columns, params, values_pyformat)
317-
insert_sql_preamble = sanitize_literals_for_upload(insert_sql_preamble)
318-
for row in rows_list:
319-
sql_params_list.append((insert_sql_preamble, row))
320311

321-
return {"sql_params_list": sql_params_list}
312+
return {
313+
"homogenous": True,
314+
"table": match.group("table_name"),
315+
"columns": columns,
316+
"values": rows_list,
317+
}
322318

323319
# Case d)
324320
# insert_sql is of the form:

google/cloud/spanner_v1/_helpers.py

Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def _make_value_pb(value):
118118
if isinstance(value, datetime_helpers.DatetimeWithNanoseconds):
119119
return Value(string_value=value.rfc3339())
120120
if isinstance(value, datetime.datetime):
121-
return Value(string_value=_datetime_to_rfc3339(value))
121+
return Value(string_value=_datetime_to_rfc3339(value, ignore_zone=False))
122122
if isinstance(value, datetime.date):
123123
return Value(string_value=value.isoformat())
124124
if isinstance(value, six.binary_type):
@@ -161,41 +161,6 @@ def _make_list_value_pbs(values):
161161

162162

163163
# pylint: disable=too-many-branches
164-
def _parse_value(value, field_type):
165-
if value is None:
166-
return None
167-
if field_type.code == TypeCode.STRING:
168-
result = value
169-
elif field_type.code == TypeCode.BYTES:
170-
result = value.encode("utf8")
171-
elif field_type.code == TypeCode.BOOL:
172-
result = value
173-
elif field_type.code == TypeCode.INT64:
174-
result = int(value)
175-
elif field_type.code == TypeCode.FLOAT64:
176-
if isinstance(value, str):
177-
result = float(value)
178-
else:
179-
result = value
180-
elif field_type.code == TypeCode.DATE:
181-
result = _date_from_iso8601_date(value)
182-
elif field_type.code == TypeCode.TIMESTAMP:
183-
DatetimeWithNanoseconds = datetime_helpers.DatetimeWithNanoseconds
184-
result = DatetimeWithNanoseconds.from_rfc3339(value)
185-
elif field_type.code == TypeCode.ARRAY:
186-
result = [_parse_value(item, field_type.array_element_type) for item in value]
187-
elif field_type.code == TypeCode.STRUCT:
188-
result = [
189-
_parse_value(item, field_type.struct_type.fields[i].type_)
190-
for (i, item) in enumerate(value)
191-
]
192-
elif field_type.code == TypeCode.NUMERIC:
193-
result = decimal.Decimal(value)
194-
else:
195-
raise ValueError("Unknown type: %s" % (field_type,))
196-
return result
197-
198-
199164
def _parse_value_pb(value_pb, field_type):
200165
"""Convert a Value protobuf to cell data.
201166
@@ -209,17 +174,41 @@ def _parse_value_pb(value_pb, field_type):
209174
:returns: value extracted from value_pb
210175
:raises ValueError: if unknown type is passed
211176
"""
177+
type_code = field_type.code
212178
if value_pb.HasField("null_value"):
213179
return None
214-
if value_pb.HasField("string_value"):
215-
return _parse_value(value_pb.string_value, field_type)
216-
if value_pb.HasField("bool_value"):
217-
return _parse_value(value_pb.bool_value, field_type)
218-
if value_pb.HasField("number_value"):
219-
return _parse_value(value_pb.number_value, field_type)
220-
if value_pb.HasField("list_value"):
221-
return _parse_value(value_pb.list_value, field_type)
222-
raise ValueError("No value set in Value: %s" % (value_pb,))
180+
if type_code == TypeCode.STRING:
181+
return value_pb.string_value
182+
elif type_code == TypeCode.BYTES:
183+
return value_pb.string_value.encode("utf8")
184+
elif type_code == TypeCode.BOOL:
185+
return value_pb.bool_value
186+
elif type_code == TypeCode.INT64:
187+
return int(value_pb.string_value)
188+
elif type_code == TypeCode.FLOAT64:
189+
if value_pb.HasField("string_value"):
190+
return float(value_pb.string_value)
191+
else:
192+
return value_pb.number_value
193+
elif type_code == TypeCode.DATE:
194+
return _date_from_iso8601_date(value_pb.string_value)
195+
elif type_code == TypeCode.TIMESTAMP:
196+
DatetimeWithNanoseconds = datetime_helpers.DatetimeWithNanoseconds
197+
return DatetimeWithNanoseconds.from_rfc3339(value_pb.string_value)
198+
elif type_code == TypeCode.ARRAY:
199+
return [
200+
_parse_value_pb(item_pb, field_type.array_element_type)
201+
for item_pb in value_pb.list_value.values
202+
]
203+
elif type_code == TypeCode.STRUCT:
204+
return [
205+
_parse_value_pb(item_pb, field_type.struct_type.fields[i].type_)
206+
for (i, item_pb) in enumerate(value_pb.list_value.values)
207+
]
208+
elif field_type.code == TypeCode.NUMERIC:
209+
return decimal.Decimal(value_pb.string_value)
210+
else:
211+
raise ValueError("Unknown type: %s" % (field_type,))
223212

224213

225214
# pylint: enable=too-many-branches

google/cloud/spanner_v1/backup.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,23 @@ class Backup(object):
5151
:param expire_time: (Optional) The expire time that will be used to
5252
create the backup. Required if the create method
5353
needs to be called.
54+
55+
:type version_time: :class:`datetime.datetime`
56+
:param version_time: (Optional) The version time that was specified for
57+
the externally consistent copy of the database. If
58+
not present, it is the same as the `create_time` of
59+
the backup.
5460
"""
5561

56-
def __init__(self, backup_id, instance, database="", expire_time=None):
62+
def __init__(
63+
self, backup_id, instance, database="", expire_time=None, version_time=None
64+
):
5765
self.backup_id = backup_id
5866
self._instance = instance
5967
self._database = database
6068
self._expire_time = expire_time
6169
self._create_time = None
70+
self._version_time = version_time
6271
self._size_bytes = None
6372
self._state = None
6473
self._referencing_databases = None
@@ -109,6 +118,16 @@ def create_time(self):
109118
"""
110119
return self._create_time
111120

121+
@property
122+
def version_time(self):
123+
"""Version time of this backup.
124+
125+
:rtype: :class:`datetime.datetime`
126+
:returns: a datetime object representing the version time of
127+
this backup
128+
"""
129+
return self._version_time
130+
112131
@property
113132
def size_bytes(self):
114133
"""Size of this backup in bytes.
@@ -190,7 +209,11 @@ def create(self):
190209
raise ValueError("database not set")
191210
api = self._instance._client.database_admin_api
192211
metadata = _metadata_with_prefix(self.name)
193-
backup = BackupPB(database=self._database, expire_time=self.expire_time,)
212+
backup = BackupPB(
213+
database=self._database,
214+
expire_time=self.expire_time,
215+
version_time=self.version_time,
216+
)
194217

195218
future = api.create_backup(
196219
parent=self._instance.name,
@@ -228,6 +251,7 @@ def reload(self):
228251
self._database = pb.database
229252
self._expire_time = pb.expire_time
230253
self._create_time = pb.create_time
254+
self._version_time = pb.version_time
231255
self._size_bytes = pb.size_bytes
232256
self._state = BackupPB.State(pb.state)
233257
self._referencing_databases = pb.referencing_databases

google/cloud/spanner_v1/batch.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""Context manager for Cloud Spanner batched writes."""
1616

17+
from google.cloud.spanner_v1 import CommitRequest
1718
from google.cloud.spanner_v1 import Mutation
1819
from google.cloud.spanner_v1 import TransactionOptions
1920

@@ -123,6 +124,7 @@ class Batch(_BatchBase):
123124
"""
124125

125126
committed = None
127+
commit_stats = None
126128
"""Timestamp at which the batch was successfully committed."""
127129

128130
def _check_state(self):
@@ -136,9 +138,13 @@ def _check_state(self):
136138
if self.committed is not None:
137139
raise ValueError("Batch already committed")
138140

139-
def commit(self):
141+
def commit(self, return_commit_stats=False):
140142
"""Commit mutations to the database.
141143
144+
:type return_commit_stats: bool
145+
:param return_commit_stats:
146+
If true, the response will return commit stats which can be accessed though commit_stats.
147+
142148
:rtype: datetime
143149
:returns: timestamp of the committed changes.
144150
"""
@@ -148,14 +154,16 @@ def commit(self):
148154
metadata = _metadata_with_prefix(database.name)
149155
txn_options = TransactionOptions(read_write=TransactionOptions.ReadWrite())
150156
trace_attributes = {"num_mutations": len(self._mutations)}
157+
request = CommitRequest(
158+
session=self._session.name,
159+
mutations=self._mutations,
160+
single_use_transaction=txn_options,
161+
return_commit_stats=return_commit_stats,
162+
)
151163
with trace_call("CloudSpanner.Commit", self._session, trace_attributes):
152-
response = api.commit(
153-
session=self._session.name,
154-
mutations=self._mutations,
155-
single_use_transaction=txn_options,
156-
metadata=metadata,
157-
)
164+
response = api.commit(request=request, metadata=metadata,)
158165
self.committed = response.commit_timestamp
166+
self.commit_stats = response.commit_stats
159167
return self.committed
160168

161169
def __enter__(self):

0 commit comments

Comments
 (0)