google-cloud-spanner 3.55.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google/cloud/spanner.py +47 -0
- google/cloud/spanner_admin_database_v1/__init__.py +146 -0
- google/cloud/spanner_admin_database_v1/gapic_metadata.json +418 -0
- google/cloud/spanner_admin_database_v1/gapic_version.py +16 -0
- google/cloud/spanner_admin_database_v1/py.typed +2 -0
- google/cloud/spanner_admin_database_v1/services/__init__.py +15 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/__init__.py +22 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py +4097 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/client.py +4602 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/pagers.py +989 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/__init__.py +38 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/base.py +820 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc.py +1303 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc_asyncio.py +1688 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest.py +6512 -0
- google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest_base.py +1650 -0
- google/cloud/spanner_admin_database_v1/types/__init__.py +144 -0
- google/cloud/spanner_admin_database_v1/types/backup.py +1106 -0
- google/cloud/spanner_admin_database_v1/types/backup_schedule.py +369 -0
- google/cloud/spanner_admin_database_v1/types/common.py +180 -0
- google/cloud/spanner_admin_database_v1/types/spanner_database_admin.py +1303 -0
- google/cloud/spanner_admin_instance_v1/__init__.py +110 -0
- google/cloud/spanner_admin_instance_v1/gapic_metadata.json +343 -0
- google/cloud/spanner_admin_instance_v1/gapic_version.py +16 -0
- google/cloud/spanner_admin_instance_v1/py.typed +2 -0
- google/cloud/spanner_admin_instance_v1/services/__init__.py +15 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/__init__.py +22 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/async_client.py +3466 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/client.py +3881 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/pagers.py +856 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/__init__.py +38 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/base.py +545 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc.py +1347 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc_asyncio.py +1539 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest.py +4834 -0
- google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest_base.py +1198 -0
- google/cloud/spanner_admin_instance_v1/types/__init__.py +104 -0
- google/cloud/spanner_admin_instance_v1/types/common.py +99 -0
- google/cloud/spanner_admin_instance_v1/types/spanner_instance_admin.py +2375 -0
- google/cloud/spanner_dbapi/__init__.py +93 -0
- google/cloud/spanner_dbapi/_helpers.py +113 -0
- google/cloud/spanner_dbapi/batch_dml_executor.py +135 -0
- google/cloud/spanner_dbapi/checksum.py +80 -0
- google/cloud/spanner_dbapi/client_side_statement_executor.py +140 -0
- google/cloud/spanner_dbapi/client_side_statement_parser.py +106 -0
- google/cloud/spanner_dbapi/connection.py +818 -0
- google/cloud/spanner_dbapi/cursor.py +609 -0
- google/cloud/spanner_dbapi/exceptions.py +172 -0
- google/cloud/spanner_dbapi/parse_utils.py +392 -0
- google/cloud/spanner_dbapi/parsed_statement.py +63 -0
- google/cloud/spanner_dbapi/parser.py +258 -0
- google/cloud/spanner_dbapi/partition_helper.py +41 -0
- google/cloud/spanner_dbapi/transaction_helper.py +294 -0
- google/cloud/spanner_dbapi/types.py +106 -0
- google/cloud/spanner_dbapi/utils.py +147 -0
- google/cloud/spanner_dbapi/version.py +20 -0
- google/cloud/spanner_v1/__init__.py +154 -0
- google/cloud/spanner_v1/_helpers.py +751 -0
- google/cloud/spanner_v1/_opentelemetry_tracing.py +165 -0
- google/cloud/spanner_v1/backup.py +397 -0
- google/cloud/spanner_v1/batch.py +433 -0
- google/cloud/spanner_v1/client.py +538 -0
- google/cloud/spanner_v1/data_types.py +350 -0
- google/cloud/spanner_v1/database.py +1968 -0
- google/cloud/spanner_v1/database_sessions_manager.py +249 -0
- google/cloud/spanner_v1/gapic_metadata.json +268 -0
- google/cloud/spanner_v1/gapic_version.py +16 -0
- google/cloud/spanner_v1/instance.py +735 -0
- google/cloud/spanner_v1/keyset.py +193 -0
- google/cloud/spanner_v1/merged_result_set.py +146 -0
- google/cloud/spanner_v1/metrics/constants.py +71 -0
- google/cloud/spanner_v1/metrics/metrics_capture.py +75 -0
- google/cloud/spanner_v1/metrics/metrics_exporter.py +384 -0
- google/cloud/spanner_v1/metrics/metrics_interceptor.py +156 -0
- google/cloud/spanner_v1/metrics/metrics_tracer.py +588 -0
- google/cloud/spanner_v1/metrics/metrics_tracer_factory.py +328 -0
- google/cloud/spanner_v1/metrics/spanner_metrics_tracer_factory.py +172 -0
- google/cloud/spanner_v1/param_types.py +110 -0
- google/cloud/spanner_v1/pool.py +813 -0
- google/cloud/spanner_v1/py.typed +2 -0
- google/cloud/spanner_v1/request_id_header.py +64 -0
- google/cloud/spanner_v1/services/__init__.py +15 -0
- google/cloud/spanner_v1/services/spanner/__init__.py +22 -0
- google/cloud/spanner_v1/services/spanner/async_client.py +2205 -0
- google/cloud/spanner_v1/services/spanner/client.py +2624 -0
- google/cloud/spanner_v1/services/spanner/pagers.py +196 -0
- google/cloud/spanner_v1/services/spanner/transports/__init__.py +38 -0
- google/cloud/spanner_v1/services/spanner/transports/base.py +520 -0
- google/cloud/spanner_v1/services/spanner/transports/grpc.py +911 -0
- google/cloud/spanner_v1/services/spanner/transports/grpc_asyncio.py +1144 -0
- google/cloud/spanner_v1/services/spanner/transports/rest.py +3468 -0
- google/cloud/spanner_v1/services/spanner/transports/rest_base.py +981 -0
- google/cloud/spanner_v1/session.py +631 -0
- google/cloud/spanner_v1/session_options.py +133 -0
- google/cloud/spanner_v1/snapshot.py +1057 -0
- google/cloud/spanner_v1/streamed.py +402 -0
- google/cloud/spanner_v1/table.py +181 -0
- google/cloud/spanner_v1/testing/__init__.py +0 -0
- google/cloud/spanner_v1/testing/database_test.py +121 -0
- google/cloud/spanner_v1/testing/interceptors.py +118 -0
- google/cloud/spanner_v1/testing/mock_database_admin.py +38 -0
- google/cloud/spanner_v1/testing/mock_spanner.py +261 -0
- google/cloud/spanner_v1/testing/spanner_database_admin_pb2_grpc.py +1267 -0
- google/cloud/spanner_v1/testing/spanner_pb2_grpc.py +882 -0
- google/cloud/spanner_v1/transaction.py +747 -0
- google/cloud/spanner_v1/types/__init__.py +118 -0
- google/cloud/spanner_v1/types/commit_response.py +94 -0
- google/cloud/spanner_v1/types/keys.py +248 -0
- google/cloud/spanner_v1/types/mutation.py +201 -0
- google/cloud/spanner_v1/types/query_plan.py +220 -0
- google/cloud/spanner_v1/types/result_set.py +379 -0
- google/cloud/spanner_v1/types/spanner.py +1815 -0
- google/cloud/spanner_v1/types/transaction.py +818 -0
- google/cloud/spanner_v1/types/type.py +288 -0
- google_cloud_spanner-3.55.0.dist-info/LICENSE +202 -0
- google_cloud_spanner-3.55.0.dist-info/METADATA +318 -0
- google_cloud_spanner-3.55.0.dist-info/RECORD +119 -0
- google_cloud_spanner-3.55.0.dist-info/WHEEL +5 -0
- google_cloud_spanner-3.55.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Spanner DB API exceptions."""
|
|
16
|
+
|
|
17
|
+
from google.api_core.exceptions import GoogleAPICallError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Warning(Exception):
|
|
21
|
+
"""Important DB API warning."""
|
|
22
|
+
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Error(Exception):
|
|
27
|
+
"""The base class for all the DB API exceptions.
|
|
28
|
+
|
|
29
|
+
Does not include :class:`Warning`.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def _is_error_cause_instance_of_google_api_exception(self):
|
|
33
|
+
return isinstance(self.__cause__, GoogleAPICallError)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def reason(self):
|
|
37
|
+
"""The reason of the error.
|
|
38
|
+
Reference:
|
|
39
|
+
https://cloud.google.com/apis/design/errors#error_info
|
|
40
|
+
Returns:
|
|
41
|
+
Union[str, None]: An optional string containing reason of the error.
|
|
42
|
+
"""
|
|
43
|
+
return (
|
|
44
|
+
self.__cause__.reason
|
|
45
|
+
if self._is_error_cause_instance_of_google_api_exception()
|
|
46
|
+
else None
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def domain(self):
|
|
51
|
+
"""The logical grouping to which the "reason" belongs.
|
|
52
|
+
Reference:
|
|
53
|
+
https://cloud.google.com/apis/design/errors#error_info
|
|
54
|
+
Returns:
|
|
55
|
+
Union[str, None]: An optional string containing a logical grouping to which the "reason" belongs.
|
|
56
|
+
"""
|
|
57
|
+
return (
|
|
58
|
+
self.__cause__.domain
|
|
59
|
+
if self._is_error_cause_instance_of_google_api_exception()
|
|
60
|
+
else None
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def metadata(self):
|
|
65
|
+
"""Additional structured details about this error.
|
|
66
|
+
Reference:
|
|
67
|
+
https://cloud.google.com/apis/design/errors#error_info
|
|
68
|
+
Returns:
|
|
69
|
+
Union[Dict[str, str], None]: An optional object containing structured details about the error.
|
|
70
|
+
"""
|
|
71
|
+
return (
|
|
72
|
+
self.__cause__.metadata
|
|
73
|
+
if self._is_error_cause_instance_of_google_api_exception()
|
|
74
|
+
else None
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def details(self):
|
|
79
|
+
"""Information contained in google.rpc.status.details.
|
|
80
|
+
Reference:
|
|
81
|
+
https://cloud.google.com/apis/design/errors#error_model
|
|
82
|
+
https://cloud.google.com/apis/design/errors#error_details
|
|
83
|
+
Returns:
|
|
84
|
+
Sequence[Any]: A list of structured objects from error_details.proto
|
|
85
|
+
"""
|
|
86
|
+
return (
|
|
87
|
+
self.__cause__.details
|
|
88
|
+
if self._is_error_cause_instance_of_google_api_exception()
|
|
89
|
+
else None
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class InterfaceError(Error):
|
|
94
|
+
"""
|
|
95
|
+
Error related to the database interface
|
|
96
|
+
rather than the database itself.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class DatabaseError(Error):
|
|
103
|
+
"""Error related to the database."""
|
|
104
|
+
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DataError(DatabaseError):
|
|
109
|
+
"""
|
|
110
|
+
Error due to problems with the processed data like
|
|
111
|
+
division by zero, numeric value out of range, etc.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class OperationalError(DatabaseError):
|
|
118
|
+
"""
|
|
119
|
+
Error related to the database's operation, e.g. an
|
|
120
|
+
unexpected disconnect, the data source name is not
|
|
121
|
+
found, a transaction could not be processed, a
|
|
122
|
+
memory allocation error, etc.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class IntegrityError(DatabaseError):
|
|
129
|
+
"""
|
|
130
|
+
Error for cases of relational integrity of the database
|
|
131
|
+
is affected, e.g. a foreign key check fails.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class InternalError(DatabaseError):
|
|
138
|
+
"""
|
|
139
|
+
Internal database error, e.g. the cursor is not valid
|
|
140
|
+
anymore, the transaction is out of sync, etc.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ProgrammingError(DatabaseError):
|
|
147
|
+
"""
|
|
148
|
+
Programming error, e.g. table not found or already
|
|
149
|
+
exists, syntax error in the SQL statement, wrong
|
|
150
|
+
number of parameters specified, etc.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class NotSupportedError(DatabaseError):
|
|
157
|
+
"""
|
|
158
|
+
Error for case of a method or database API not
|
|
159
|
+
supported by the database was used.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class RetryAborted(OperationalError):
|
|
166
|
+
"""
|
|
167
|
+
Error for case of no aborted transaction retry
|
|
168
|
+
is available, because of underlying data being
|
|
169
|
+
changed during a retry.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
pass
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# Copyright 2020 Google LLC All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"SQL parsing and classification utils."
|
|
16
|
+
|
|
17
|
+
import datetime
|
|
18
|
+
import decimal
|
|
19
|
+
import re
|
|
20
|
+
import warnings
|
|
21
|
+
|
|
22
|
+
import sqlparse
|
|
23
|
+
from google.cloud import spanner_v1 as spanner
|
|
24
|
+
from google.cloud.spanner_v1 import JsonObject
|
|
25
|
+
from . import client_side_statement_parser
|
|
26
|
+
|
|
27
|
+
from .exceptions import Error
|
|
28
|
+
from .parsed_statement import ParsedStatement, StatementType, Statement
|
|
29
|
+
from .types import DateStr, TimestampStr
|
|
30
|
+
from .utils import sanitize_literals_for_upload
|
|
31
|
+
|
|
32
|
+
# Note: This mapping deliberately does not contain a value for float.
|
|
33
|
+
# The reason for that is that it is better to just let Spanner determine
|
|
34
|
+
# the parameter type instead of specifying one explicitly. The reason for
|
|
35
|
+
# this is that if the client specifies FLOAT64, and the actual column that
|
|
36
|
+
# the parameter is used for is of type FLOAT32, then Spanner will return an
|
|
37
|
+
# error. If however the client does not specify a type, then Spanner will
|
|
38
|
+
# automatically choose the appropriate type based on the column where the
|
|
39
|
+
# value will be inserted/updated or that it will be compared with.
|
|
40
|
+
TYPES_MAP = {
|
|
41
|
+
bool: spanner.param_types.BOOL,
|
|
42
|
+
bytes: spanner.param_types.BYTES,
|
|
43
|
+
str: spanner.param_types.STRING,
|
|
44
|
+
int: spanner.param_types.INT64,
|
|
45
|
+
datetime.datetime: spanner.param_types.TIMESTAMP,
|
|
46
|
+
datetime.date: spanner.param_types.DATE,
|
|
47
|
+
DateStr: spanner.param_types.DATE,
|
|
48
|
+
TimestampStr: spanner.param_types.TIMESTAMP,
|
|
49
|
+
decimal.Decimal: spanner.param_types.NUMERIC,
|
|
50
|
+
JsonObject: spanner.param_types.JSON,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
SPANNER_RESERVED_KEYWORDS = {
|
|
54
|
+
"ALL",
|
|
55
|
+
"AND",
|
|
56
|
+
"ANY",
|
|
57
|
+
"ARRAY",
|
|
58
|
+
"AS",
|
|
59
|
+
"ASC",
|
|
60
|
+
"ASSERT_ROWS_MODIFIED",
|
|
61
|
+
"AT",
|
|
62
|
+
"BETWEEN",
|
|
63
|
+
"BY",
|
|
64
|
+
"CASE",
|
|
65
|
+
"CAST",
|
|
66
|
+
"COLLATE",
|
|
67
|
+
"CONTAINS",
|
|
68
|
+
"CREATE",
|
|
69
|
+
"CROSS",
|
|
70
|
+
"CUBE",
|
|
71
|
+
"CURRENT",
|
|
72
|
+
"DEFAULT",
|
|
73
|
+
"DEFINE",
|
|
74
|
+
"DESC",
|
|
75
|
+
"DISTINCT",
|
|
76
|
+
"DROP",
|
|
77
|
+
"ELSE",
|
|
78
|
+
"END",
|
|
79
|
+
"ENUM",
|
|
80
|
+
"ESCAPE",
|
|
81
|
+
"EXCEPT",
|
|
82
|
+
"EXCLUDE",
|
|
83
|
+
"EXISTS",
|
|
84
|
+
"EXTRACT",
|
|
85
|
+
"FALSE",
|
|
86
|
+
"FETCH",
|
|
87
|
+
"FOLLOWING",
|
|
88
|
+
"FOR",
|
|
89
|
+
"FROM",
|
|
90
|
+
"FULL",
|
|
91
|
+
"GROUP",
|
|
92
|
+
"GROUPING",
|
|
93
|
+
"GROUPS",
|
|
94
|
+
"HASH",
|
|
95
|
+
"HAVING",
|
|
96
|
+
"IF",
|
|
97
|
+
"IGNORE",
|
|
98
|
+
"IN",
|
|
99
|
+
"INNER",
|
|
100
|
+
"INTERSECT",
|
|
101
|
+
"INTERVAL",
|
|
102
|
+
"INTO",
|
|
103
|
+
"IS",
|
|
104
|
+
"JOIN",
|
|
105
|
+
"LATERAL",
|
|
106
|
+
"LEFT",
|
|
107
|
+
"LIKE",
|
|
108
|
+
"LIMIT",
|
|
109
|
+
"LOOKUP",
|
|
110
|
+
"MERGE",
|
|
111
|
+
"NATURAL",
|
|
112
|
+
"NEW",
|
|
113
|
+
"NO",
|
|
114
|
+
"NOT",
|
|
115
|
+
"NULL",
|
|
116
|
+
"NULLS",
|
|
117
|
+
"OF",
|
|
118
|
+
"ON",
|
|
119
|
+
"OR",
|
|
120
|
+
"ORDER",
|
|
121
|
+
"OUTER",
|
|
122
|
+
"OVER",
|
|
123
|
+
"PARTITION",
|
|
124
|
+
"PRECEDING",
|
|
125
|
+
"PROTO",
|
|
126
|
+
"RANGE",
|
|
127
|
+
"RECURSIVE",
|
|
128
|
+
"RESPECT",
|
|
129
|
+
"RIGHT",
|
|
130
|
+
"ROLLUP",
|
|
131
|
+
"ROWS",
|
|
132
|
+
"SELECT",
|
|
133
|
+
"SET",
|
|
134
|
+
"SOME",
|
|
135
|
+
"STRUCT",
|
|
136
|
+
"TABLESAMPLE",
|
|
137
|
+
"THEN",
|
|
138
|
+
"TO",
|
|
139
|
+
"TREAT",
|
|
140
|
+
"TRUE",
|
|
141
|
+
"UNBOUNDED",
|
|
142
|
+
"UNION",
|
|
143
|
+
"UNNEST",
|
|
144
|
+
"USING",
|
|
145
|
+
"WHEN",
|
|
146
|
+
"WHERE",
|
|
147
|
+
"WINDOW",
|
|
148
|
+
"WITH",
|
|
149
|
+
"WITHIN",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
STMT_DDL = "DDL"
|
|
153
|
+
STMT_NON_UPDATING = "NON_UPDATING"
|
|
154
|
+
STMT_UPDATING = "UPDATING"
|
|
155
|
+
STMT_INSERT = "INSERT"
|
|
156
|
+
|
|
157
|
+
# Heuristic for identifying statements that don't need to be run as updates.
|
|
158
|
+
# TODO: This and the other regexes do not match statements that start with a hint.
|
|
159
|
+
RE_NON_UPDATE = re.compile(r"^\W*(SELECT|GRAPH|FROM)", re.IGNORECASE)
|
|
160
|
+
|
|
161
|
+
RE_WITH = re.compile(r"^\s*(WITH)", re.IGNORECASE)
|
|
162
|
+
|
|
163
|
+
# DDL statements follow
|
|
164
|
+
# https://cloud.google.com/spanner/docs/data-definition-language
|
|
165
|
+
RE_DDL = re.compile(
|
|
166
|
+
r"^\s*(CREATE|ALTER|DROP|GRANT|REVOKE|RENAME|ANALYZE)", re.IGNORECASE | re.DOTALL
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# TODO: These do not match statements that start with a hint.
|
|
170
|
+
RE_IS_INSERT = re.compile(r"^\s*(INSERT\s+)", re.IGNORECASE | re.DOTALL)
|
|
171
|
+
RE_IS_UPDATE = re.compile(r"^\s*(UPDATE\s+)", re.IGNORECASE | re.DOTALL)
|
|
172
|
+
RE_IS_DELETE = re.compile(r"^\s*(DELETE\s+)", re.IGNORECASE | re.DOTALL)
|
|
173
|
+
|
|
174
|
+
RE_INSERT = re.compile(
|
|
175
|
+
# Only match the `INSERT INTO <table_name> (columns...)
|
|
176
|
+
# otherwise the rest of the statement could be a complex
|
|
177
|
+
# operation.
|
|
178
|
+
r"^\s*INSERT(?:\s+INTO)?\s+(?P<table_name>[^\s\(\)]+)\s*\((?P<columns>[^\(\)]+)\)",
|
|
179
|
+
re.IGNORECASE | re.DOTALL,
|
|
180
|
+
)
|
|
181
|
+
"""Deprecated: Use the RE_IS_INSERT, RE_IS_UPDATE, and RE_IS_DELETE regexes"""
|
|
182
|
+
|
|
183
|
+
RE_VALUES_TILL_END = re.compile(r"VALUES\s*\(.+$", re.IGNORECASE | re.DOTALL)
|
|
184
|
+
|
|
185
|
+
RE_VALUES_PYFORMAT = re.compile(
|
|
186
|
+
# To match: (%s, %s,....%s)
|
|
187
|
+
r"(\(\s*%s[^\(\)]+\))",
|
|
188
|
+
re.DOTALL,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
RE_PYFORMAT = re.compile(r"(%s|%\([^\(\)]+\)s)+", re.DOTALL)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def classify_stmt(query):
|
|
195
|
+
"""Determine SQL query type.
|
|
196
|
+
:type query: str
|
|
197
|
+
:param query: A SQL query.
|
|
198
|
+
:rtype: str
|
|
199
|
+
:returns: The query type name.
|
|
200
|
+
"""
|
|
201
|
+
warnings.warn(
|
|
202
|
+
"This method is deprecated. Use _classify_stmt method", DeprecationWarning
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# sqlparse will strip Cloud Spanner comments,
|
|
206
|
+
# still, special commenting styles, like
|
|
207
|
+
# PostgreSQL dollar quoted comments are not
|
|
208
|
+
# supported and will not be stripped.
|
|
209
|
+
query = sqlparse.format(query, strip_comments=True).strip()
|
|
210
|
+
|
|
211
|
+
if RE_DDL.match(query):
|
|
212
|
+
return STMT_DDL
|
|
213
|
+
|
|
214
|
+
if RE_IS_INSERT.match(query):
|
|
215
|
+
return STMT_INSERT
|
|
216
|
+
|
|
217
|
+
if RE_NON_UPDATE.match(query) or RE_WITH.match(query):
|
|
218
|
+
# As of 13-March-2020, Cloud Spanner only supports WITH for DQL
|
|
219
|
+
# statements and doesn't yet support WITH for DML statements.
|
|
220
|
+
return STMT_NON_UPDATING
|
|
221
|
+
|
|
222
|
+
return STMT_UPDATING
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def classify_statement(query, args=None):
|
|
226
|
+
"""Determine SQL query type.
|
|
227
|
+
|
|
228
|
+
It is an internal method that can make backwards-incompatible changes.
|
|
229
|
+
|
|
230
|
+
:type query: str
|
|
231
|
+
:param query: A SQL query.
|
|
232
|
+
|
|
233
|
+
:rtype: ParsedStatement
|
|
234
|
+
:returns: parsed statement attributes.
|
|
235
|
+
"""
|
|
236
|
+
# sqlparse will strip Cloud Spanner comments,
|
|
237
|
+
# still, special commenting styles, like
|
|
238
|
+
# PostgreSQL dollar quoted comments are not
|
|
239
|
+
# supported and will not be stripped.
|
|
240
|
+
query = sqlparse.format(query, strip_comments=True).strip()
|
|
241
|
+
if query == "":
|
|
242
|
+
return None
|
|
243
|
+
parsed_statement: ParsedStatement = client_side_statement_parser.parse_stmt(query)
|
|
244
|
+
if parsed_statement is not None:
|
|
245
|
+
return parsed_statement
|
|
246
|
+
query, args = sql_pyformat_args_to_spanner(query, args or None)
|
|
247
|
+
statement = Statement(
|
|
248
|
+
query,
|
|
249
|
+
args,
|
|
250
|
+
get_param_types(args or None),
|
|
251
|
+
)
|
|
252
|
+
statement_type = _get_statement_type(statement)
|
|
253
|
+
return ParsedStatement(statement_type, statement)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _get_statement_type(statement):
|
|
257
|
+
query = statement.sql
|
|
258
|
+
if RE_DDL.match(query):
|
|
259
|
+
return StatementType.DDL
|
|
260
|
+
if RE_IS_INSERT.match(query):
|
|
261
|
+
return StatementType.INSERT
|
|
262
|
+
if RE_NON_UPDATE.match(query) or RE_WITH.match(query):
|
|
263
|
+
# As of 13-March-2020, Cloud Spanner only supports WITH for DQL
|
|
264
|
+
# statements and doesn't yet support WITH for DML statements.
|
|
265
|
+
return StatementType.QUERY
|
|
266
|
+
|
|
267
|
+
if RE_IS_UPDATE.match(query) or RE_IS_DELETE.match(query):
|
|
268
|
+
# TODO: Remove this? It makes more sense to have this in SQLAlchemy and
|
|
269
|
+
# Django than here.
|
|
270
|
+
statement.sql = ensure_where_clause(query)
|
|
271
|
+
return StatementType.UPDATE
|
|
272
|
+
|
|
273
|
+
return StatementType.UNKNOWN
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def sql_pyformat_args_to_spanner(sql, params):
|
|
277
|
+
"""
|
|
278
|
+
Transform pyformat set SQL to named arguments for Cloud Spanner.
|
|
279
|
+
It will also unescape previously escaped format specifiers
|
|
280
|
+
like %%s to %s.
|
|
281
|
+
For example:
|
|
282
|
+
SQL: 'SELECT * from t where f1=%s, f2=%s, f3=%s'
|
|
283
|
+
Params: ('a', 23, '888***')
|
|
284
|
+
becomes:
|
|
285
|
+
SQL: 'SELECT * from t where f1=@a0, f2=@a1, f3=@a2'
|
|
286
|
+
Params: {'a0': 'a', 'a1': 23, 'a2': '888***'}
|
|
287
|
+
|
|
288
|
+
OR
|
|
289
|
+
SQL: 'SELECT * from t where f1=%(f1)s, f2=%(f2)s, f3=%(f3)s'
|
|
290
|
+
Params: {'f1': 'a', 'f2': 23, 'f3': '888***', 'extra': 'aye')
|
|
291
|
+
becomes:
|
|
292
|
+
SQL: 'SELECT * from t where f1=@a0, f2=@a1, f3=@a2'
|
|
293
|
+
Params: {'a0': 'a', 'a1': 23, 'a2': '888***'}
|
|
294
|
+
|
|
295
|
+
:type sql: str
|
|
296
|
+
:param sql: A SQL request.
|
|
297
|
+
|
|
298
|
+
:type params: list
|
|
299
|
+
:param params: A list of parameters.
|
|
300
|
+
|
|
301
|
+
:rtype: tuple(str, dict)
|
|
302
|
+
:returns: A tuple of the sanitized SQL and a dictionary of the named
|
|
303
|
+
arguments.
|
|
304
|
+
"""
|
|
305
|
+
if not params:
|
|
306
|
+
return sanitize_literals_for_upload(sql), None
|
|
307
|
+
|
|
308
|
+
found_pyformat_placeholders = RE_PYFORMAT.findall(sql)
|
|
309
|
+
params_is_dict = isinstance(params, dict)
|
|
310
|
+
|
|
311
|
+
if params_is_dict:
|
|
312
|
+
if not found_pyformat_placeholders:
|
|
313
|
+
return sanitize_literals_for_upload(sql), params
|
|
314
|
+
else:
|
|
315
|
+
n_params = len(params) if params else 0
|
|
316
|
+
n_matches = len(found_pyformat_placeholders)
|
|
317
|
+
if n_matches != n_params:
|
|
318
|
+
raise Error(
|
|
319
|
+
"pyformat_args mismatch\ngot %d args from %s\n"
|
|
320
|
+
"want %d args in %s"
|
|
321
|
+
% (n_matches, found_pyformat_placeholders, n_params, params)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
named_args = {}
|
|
325
|
+
# We've now got for example:
|
|
326
|
+
# Case a) Params is a non-dict
|
|
327
|
+
# SQL: 'SELECT * from t where f1=%s, f2=%s, f3=%s'
|
|
328
|
+
# Params: ('a', 23, '888***')
|
|
329
|
+
# Case b) Params is a dict and the matches are %(value)s'
|
|
330
|
+
for i, pyfmt in enumerate(found_pyformat_placeholders):
|
|
331
|
+
key = "a%d" % i
|
|
332
|
+
sql = sql.replace(pyfmt, "@" + key, 1)
|
|
333
|
+
if params_is_dict:
|
|
334
|
+
# The '%(key)s' case, so interpolate it.
|
|
335
|
+
resolved_value = pyfmt % params
|
|
336
|
+
named_args[key] = resolved_value
|
|
337
|
+
else:
|
|
338
|
+
named_args[key] = params[i]
|
|
339
|
+
|
|
340
|
+
return sanitize_literals_for_upload(sql), named_args
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def get_param_types(params):
|
|
344
|
+
"""Determine Cloud Spanner types for the given parameters.
|
|
345
|
+
|
|
346
|
+
:type params: dict
|
|
347
|
+
:param params: Parameters requiring to find Cloud Spanner types.
|
|
348
|
+
|
|
349
|
+
:rtype: dict
|
|
350
|
+
:returns: The types index for the given parameters.
|
|
351
|
+
"""
|
|
352
|
+
if params is None:
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
param_types = {}
|
|
356
|
+
|
|
357
|
+
for key, value in params.items():
|
|
358
|
+
type_ = type(value)
|
|
359
|
+
if type_ in TYPES_MAP:
|
|
360
|
+
param_types[key] = TYPES_MAP[type_]
|
|
361
|
+
|
|
362
|
+
return param_types
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def ensure_where_clause(sql):
|
|
366
|
+
"""
|
|
367
|
+
Cloud Spanner requires a WHERE clause on UPDATE and DELETE statements.
|
|
368
|
+
Add a dummy WHERE clause if not detected.
|
|
369
|
+
|
|
370
|
+
:type sql: str
|
|
371
|
+
:param sql: SQL code to check.
|
|
372
|
+
"""
|
|
373
|
+
if any(isinstance(token, sqlparse.sql.Where) for token in sqlparse.parse(sql)[0]):
|
|
374
|
+
return sql
|
|
375
|
+
|
|
376
|
+
return sql + " WHERE 1=1"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def escape_name(name):
|
|
380
|
+
"""
|
|
381
|
+
Apply backticks to the name that either contain '-' or
|
|
382
|
+
' ', or is a Cloud Spanner's reserved keyword.
|
|
383
|
+
|
|
384
|
+
:type name: str
|
|
385
|
+
:param name: Name to escape.
|
|
386
|
+
|
|
387
|
+
:rtype: str
|
|
388
|
+
:returns: Name escaped if it has to be escaped.
|
|
389
|
+
"""
|
|
390
|
+
if "-" in name or " " in name or name.upper() in SPANNER_RESERVED_KEYWORDS:
|
|
391
|
+
return "`" + name + "`"
|
|
392
|
+
return name
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright 2023 Google LLC All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, List
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StatementType(Enum):
|
|
20
|
+
UNKNOWN = 0
|
|
21
|
+
CLIENT_SIDE = 1
|
|
22
|
+
DDL = 2
|
|
23
|
+
QUERY = 3
|
|
24
|
+
UPDATE = 4
|
|
25
|
+
INSERT = 5
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ClientSideStatementType(Enum):
|
|
29
|
+
COMMIT = 1
|
|
30
|
+
BEGIN = 2
|
|
31
|
+
ROLLBACK = 3
|
|
32
|
+
SHOW_COMMIT_TIMESTAMP = 4
|
|
33
|
+
SHOW_READ_TIMESTAMP = 5
|
|
34
|
+
START_BATCH_DML = 6
|
|
35
|
+
RUN_BATCH = 7
|
|
36
|
+
ABORT_BATCH = 8
|
|
37
|
+
PARTITION_QUERY = 9
|
|
38
|
+
RUN_PARTITION = 10
|
|
39
|
+
RUN_PARTITIONED_QUERY = 11
|
|
40
|
+
SET_AUTOCOMMIT_DML_MODE = 12
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AutocommitDmlMode(Enum):
|
|
44
|
+
TRANSACTIONAL = 1
|
|
45
|
+
PARTITIONED_NON_ATOMIC = 2
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Statement:
|
|
50
|
+
sql: str
|
|
51
|
+
params: Any = None
|
|
52
|
+
param_types: Any = None
|
|
53
|
+
|
|
54
|
+
def get_tuple(self):
|
|
55
|
+
return self.sql, self.params, self.param_types
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ParsedStatement:
|
|
60
|
+
statement_type: StatementType
|
|
61
|
+
statement: Statement
|
|
62
|
+
client_side_statement_type: ClientSideStatementType = None
|
|
63
|
+
client_side_statement_params: List[Any] = None
|