langchain-google-spanner 0.1.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.
@@ -0,0 +1,37 @@
1
+ # Copyright 2024 Google LLC
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
+ from langchain_google_spanner.chat_message_history import SpannerChatMessageHistory
16
+ from langchain_google_spanner.vector_store import (
17
+ DistanceStrategy,
18
+ QueryParameters,
19
+ SecondaryIndex,
20
+ SpannerVectorStore,
21
+ TableColumn,
22
+ )
23
+
24
+ from .loader import SpannerDocumentSaver, SpannerLoader
25
+ from .version import __version__
26
+
27
+ __all__ = [
28
+ "__version__",
29
+ "SpannerChatMessageHistory",
30
+ "SpannerVectorStore",
31
+ "SpannerDocumentSaver",
32
+ "SpannerLoader",
33
+ "TableColumn",
34
+ "SecondaryIndex",
35
+ "QueryParameters",
36
+ "DistanceStrategy",
37
+ ]
@@ -0,0 +1,194 @@
1
+ # Copyright 2024 Google LLC
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
+ """Cloud Spanner-based chat message history"""
16
+ from __future__ import annotations
17
+
18
+ from typing import List, Optional
19
+
20
+ from google.cloud import spanner
21
+ from google.cloud.spanner_admin_database_v1.types import DatabaseDialect # type: ignore
22
+ from google.cloud.spanner_v1 import param_types
23
+ from google.cloud.spanner_v1.data_types import JsonObject
24
+ from langchain_core.chat_history import BaseChatMessageHistory
25
+ from langchain_core.messages import BaseMessage, messages_from_dict
26
+
27
+ from .version import __version__
28
+
29
+ USER_AGENT_CHAT = "langchain-google-spanner-python:chat_history" + __version__
30
+
31
+ OPERATION_TIMEOUT_SECONDS = 240
32
+
33
+ COLUMN_FAMILY = "langchain"
34
+ COLUMN_NAME = "history"
35
+
36
+
37
+ def client_with_user_agent(
38
+ client: Optional[spanner.Client], user_agent: str
39
+ ) -> spanner.Client:
40
+ if not client:
41
+ client = spanner.Client()
42
+ client_agent = client._client_info.user_agent
43
+ if not client_agent:
44
+ client._client_info.user_agent = user_agent
45
+ elif user_agent not in client_agent:
46
+ client._client_info.user_agent = " ".join([client_agent, user_agent])
47
+ return client
48
+
49
+
50
+ class SpannerChatMessageHistory(BaseChatMessageHistory):
51
+ """Chat message history that stores history in Spanner.
52
+
53
+ Args:
54
+ instance_id: The Spanner instance to use for chat message history.
55
+ database_id: The Spanner database to use for chat message history.
56
+ table_name: The Spanner table to use for chat message history.
57
+ session_id: Optional. The existing session ID.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ instance_id: str,
63
+ database_id: str,
64
+ session_id: str,
65
+ table_name: str,
66
+ client: Optional[spanner.Client] = None,
67
+ ) -> None:
68
+ self.instance_id = instance_id
69
+ self.database_id = database_id
70
+ self.session_id = session_id
71
+ self.table_name = table_name
72
+ self.client = client_with_user_agent(client, USER_AGENT_CHAT)
73
+ self.instance = self.client.instance(instance_id)
74
+ if not self.instance.exists():
75
+ raise Exception("Instance doesn't exist.")
76
+ self.database = self.instance.database(database_id)
77
+ if not self.database.exists():
78
+ raise Exception("Database doesn't exist.")
79
+ self.database.reload()
80
+ self.dialect = self.database.database_dialect
81
+ self._verify_schema()
82
+
83
+ def _verify_schema(self) -> None:
84
+ """Verify table exists with required schema for SpannerChatMessageHistory class.
85
+ Use helper method MSSQLEngine.create_chat_history_table(...) to create
86
+ table with valid schema.
87
+ """
88
+ # check table exists
89
+ column_names = [] # type: List[str]
90
+ with self.database.snapshot() as snapshot:
91
+ results = snapshot.execute_sql(
92
+ f"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.columns WHERE table_name = '{self.table_name}'"
93
+ )
94
+ for row in results:
95
+ column_names.append(*row)
96
+
97
+ # check that all required columns are present
98
+ required_columns = ["id", "session_id", "created_at", "message"]
99
+ if len(column_names) == 0:
100
+ raise AttributeError(
101
+ f"Table '{self.table_name}' does not exist. Please create "
102
+ "it before initializing SpannerChatMessageHistory. See "
103
+ "SpannerEngine.create_chat_history_table() for a helper method."
104
+ )
105
+ else:
106
+ if not (all(x in column_names for x in required_columns)):
107
+ google_schema = f"""CREATE TABLE IF NOT EXISTS {self.table_name} (
108
+ id STRING(36) DEFAULT (GENERATE_UUID()),
109
+ created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
110
+ session_id STRING(MAX) NOT NULL,
111
+ message JSON NOT NULL,
112
+ ) PRIMARY KEY (session_id, created_at ASC, id)"""
113
+ pg_schema = f"""CREATE TABLE IF NOT EXISTS {self.table_name} (
114
+ id varchar(36) DEFAULT (spanner.generate_uuid()),
115
+ created_at SPANNER.COMMIT_TIMESTAMP NOT NULL,
116
+ session_id TEXT NOT NULL,
117
+ message JSONB NOT NULL,
118
+ PRIMARY KEY (session_id, created_at, id)"""
119
+ ddl = (
120
+ pg_schema
121
+ if self.dialect == DatabaseDialect.POSTGRESQL
122
+ else google_schema
123
+ )
124
+ raise IndexError(
125
+ f"Table '{self.table_name}' has incorrect schema. Got "
126
+ f"column names '{column_names}' but required column names "
127
+ f"'{required_columns}'.\nPlease create table with following schema:"
128
+ f"{ddl};"
129
+ )
130
+
131
+ def create_chat_history_table(self) -> None:
132
+ google_schema = f"""CREATE TABLE IF NOT EXISTS {self.table_name} (
133
+ id STRING(36) DEFAULT (GENERATE_UUID()),
134
+ created_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp=true),
135
+ session_id STRING(MAX) NOT NULL,
136
+ message JSON NOT NULL,
137
+ ) PRIMARY KEY (session_id, created_at ASC, id)"""
138
+
139
+ pg_schema = f"""CREATE TABLE IF NOT EXISTS {self.table_name} (
140
+ id varchar(36) DEFAULT (spanner.generate_uuid()),
141
+ created_at SPANNER.COMMIT_TIMESTAMP NOT NULL,
142
+ session_id TEXT NOT NULL,
143
+ message JSONB NOT NULL,
144
+ PRIMARY KEY (session_id, created_at, id)
145
+ );"""
146
+
147
+ ddl = pg_schema if self.dialect == DatabaseDialect.POSTGRESQL else google_schema
148
+ database = self.client.instance(self.instance_id).database(self.database_id)
149
+ operation = database.update_ddl([ddl])
150
+ operation.result(OPERATION_TIMEOUT_SECONDS)
151
+ return operation
152
+
153
+ @property
154
+ def messages(self) -> List[BaseMessage]: # type: ignore
155
+ """Retrieve the messages from Cloud Spanner"""
156
+ place_holder = "$1" if self.dialect == DatabaseDialect.POSTGRESQL else "@p1"
157
+ query = f"SELECT message FROM {self.table_name} WHERE session_id = {place_holder} ORDER BY created_at;"
158
+ param = {"p1": self.session_id}
159
+ param_type = {"p1": param_types.STRING}
160
+
161
+ with self.database.snapshot() as snapshot:
162
+ results = snapshot.execute_sql(
163
+ query,
164
+ params=param,
165
+ param_types=param_type,
166
+ )
167
+ items = [] # type: List[dict]
168
+ for row in results:
169
+ items.append({"data": row[0], "type": row[0]["type"]})
170
+ messages = messages_from_dict(items)
171
+ return messages
172
+
173
+ def add_message(self, message: BaseMessage) -> None:
174
+ """Append the message to the record in Cloud Spanner"""
175
+ with self.database.batch() as batch:
176
+ batch.insert(
177
+ table=self.table_name,
178
+ columns=("session_id", "created_at", "message"),
179
+ values=[
180
+ (
181
+ self.session_id,
182
+ spanner.COMMIT_TIMESTAMP,
183
+ JsonObject(message.dict()),
184
+ ),
185
+ ],
186
+ )
187
+
188
+ def clear(self) -> None:
189
+ """Clear session memory from Cloud Spanner"""
190
+ place_holder = "$1" if self.dialect == DatabaseDialect.POSTGRESQL else "@p1"
191
+ query = f"DELETE FROM {self.table_name} WHERE session_id = {place_holder};"
192
+ param = {"p1": self.session_id}
193
+ param_type = {"p1": param_types.STRING}
194
+ self.database.execute_partitioned_dml(query, param, param_type)
@@ -0,0 +1,454 @@
1
+ # Copyright 2024 Google LLC
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
+ import datetime
16
+ import json
17
+ from dataclasses import dataclass
18
+ from typing import Any, Dict, Iterator, List, Optional, Union
19
+
20
+ from google.cloud.spanner import Client, KeySet # type: ignore
21
+ from google.cloud.spanner_admin_database_v1.types import DatabaseDialect # type: ignore
22
+ from google.cloud.spanner_v1.data_types import JsonObject # type: ignore
23
+ from langchain_community.document_loaders.base import BaseLoader
24
+ from langchain_core.documents import Document
25
+
26
+ from .version import __version__
27
+
28
+ USER_AGENT_LOADER = "langchain-google-spanner-python:document_loader" + __version__
29
+ USER_AGENT_SAVER = "langchain-google-spanner-python:document_saver" + __version__
30
+
31
+ OPERATION_TIMEOUT_SECONDS = 240
32
+ MUTATION_BATCH_SIZE = 1000
33
+
34
+ CONTENT_COL_NAME = "page_content"
35
+ METADATA_COL_NAME = "langchain_metadata"
36
+
37
+
38
+ @dataclass
39
+ class Column:
40
+ name: str
41
+ data_type: str
42
+ nullable: bool = True
43
+
44
+
45
+ def client_with_user_agent(client: Optional[Client], user_agent: str) -> Client:
46
+ if not client:
47
+ client = Client()
48
+
49
+ client_agent = client._client_info.user_agent
50
+ if not client_agent:
51
+ client._client_info.user_agent = user_agent
52
+ elif user_agent not in client_agent:
53
+ client._client_info.user_agent = " ".join([client_agent, user_agent])
54
+ return client
55
+
56
+
57
+ def _load_row_to_doc(
58
+ format: str,
59
+ content_columns: List[str],
60
+ metadata_columns: List[str],
61
+ metadata_json_column: str,
62
+ row: Dict,
63
+ ) -> Document:
64
+ page_content = ""
65
+ if format == "text":
66
+ page_content = " ".join(str(row[c]) for c in content_columns if row[c])
67
+ elif format == "YAML":
68
+ page_content = "\n".join(f"{c}: {str(row[c])}" for c in content_columns)
69
+ elif format == "JSON":
70
+ j = {}
71
+ for c in content_columns:
72
+ j[c] = row[c]
73
+ page_content = json.dumps(j)
74
+ elif format == "CSV":
75
+ page_content = ", ".join(str(row[c]) for c in content_columns if row[c])
76
+
77
+ metadata: Dict[str, Any] = {}
78
+ if metadata_json_column in metadata_columns and row.get(metadata_json_column):
79
+ metadata = row[metadata_json_column]
80
+ for c in metadata_columns:
81
+ if c != metadata_json_column:
82
+ metadata[c] = row[c]
83
+
84
+ return Document(page_content=page_content, metadata=metadata)
85
+
86
+
87
+ def _load_doc_to_row(
88
+ table_fields: List[str],
89
+ doc: Document,
90
+ content_column: str,
91
+ metadata_json_column: str,
92
+ parse_json: bool = True,
93
+ ) -> tuple:
94
+ """
95
+ Load document to row.
96
+
97
+ Args:
98
+ table_fields: Spanner table fields names.
99
+ doc: Document that is used.
100
+ content_column: Name of the content column.
101
+ metadata_json_column: Name of the special JSON column.
102
+ parse_json: Parse json column to string or leave it as JSON object. String format is needed to for Spanner inserts.
103
+ JSON object is used to compare with Spanner reads.
104
+ """
105
+ doc_metadata = doc.metadata.copy()
106
+ row = []
107
+ for col in table_fields:
108
+ if (
109
+ col != content_column
110
+ and col != metadata_json_column
111
+ and col in doc_metadata
112
+ ):
113
+ row.append(doc_metadata[col])
114
+ del doc_metadata[col]
115
+ if col == content_column:
116
+ row.append(doc.page_content)
117
+
118
+ if metadata_json_column in table_fields:
119
+ metadata_json = {}
120
+ if metadata_json_column in doc_metadata:
121
+ metadata_json = doc_metadata[metadata_json_column]
122
+ del doc_metadata[metadata_json_column]
123
+ metadata_json = {**metadata_json, **doc_metadata}
124
+ j = json.dumps(metadata_json) if parse_json else metadata_json
125
+ row.append(j) # type: ignore
126
+
127
+ return tuple(row)
128
+
129
+
130
+ def _batch(datas: List[Any], size: int = 1) -> Iterator[List[Any]]:
131
+ data_length = len(datas)
132
+ for current in range(0, data_length, size):
133
+ yield datas[current : min(current + size, data_length)]
134
+
135
+
136
+ class SpannerLoader(BaseLoader):
137
+ """Loads data from Google Cloud Spanner."""
138
+
139
+ def __init__(
140
+ self,
141
+ instance_id: str,
142
+ database_id: str,
143
+ query: str,
144
+ content_columns: List[str] = [],
145
+ metadata_columns: List[str] = [],
146
+ format: str = "text",
147
+ databoost: bool = False,
148
+ metadata_json_column: str = METADATA_COL_NAME,
149
+ staleness: Union[float, datetime.datetime] = 0.0,
150
+ client: Optional[Client] = None,
151
+ ):
152
+ """Initialize Spanner document loader.
153
+
154
+ Args:
155
+ instance_id: The Spanner instance to load data from.
156
+ database_id: The Spanner database to load data from.
157
+ query: A GoogleSQL or PostgreSQL query. Users must match dialect to their database.
158
+ content_columns: The list of column(s) or field(s) to use for a Document's page content.
159
+ Page content is the default field for embeddings generation.
160
+ metadata_columns: The list of column(s) or field(s) to use for metadata.
161
+ format: Set the format of page content if using multiple columns or fields.
162
+ Format included: 'text', 'JSON', 'YAML', 'CSV'.
163
+ databoost: Use data boost on read. Note: needs extra IAM permissions and higher cost.
164
+ metadata_json_column: The name of the JSON column to use as the metadata's base dictionary.
165
+ staleness: The time bound for stale read. Takes either a datetime or float.
166
+ client: The connection object to use. This can be used to customize project id and credentials.
167
+ """
168
+ self.instance_id = instance_id
169
+ self.database_id = database_id
170
+ self.query = query
171
+ self.content_columns = content_columns
172
+ self.metadata_columns = metadata_columns
173
+ self.format = format
174
+ self.metadata_json_column = metadata_json_column
175
+ self.databoost = databoost
176
+ self.client = client_with_user_agent(client, USER_AGENT_LOADER)
177
+ self.staleness = staleness
178
+
179
+ formats = ["JSON", "text", "YAML", "CSV"]
180
+ if self.format not in formats:
181
+ raise Exception("Use one of 'text', 'JSON', 'YAML', 'CSV'.")
182
+
183
+ instance = self.client.instance(instance_id)
184
+ if not instance.exists():
185
+ raise Exception("Instance doesn't exist.")
186
+
187
+ database = instance.database(database_id)
188
+ if not database.exists():
189
+ raise Exception("Database doesn't exist.")
190
+
191
+ def load(self) -> List[Document]:
192
+ """
193
+ Load langchain documents from a Spanner database.
194
+
195
+ Returns:
196
+ (List[langchain_core.documents.Document]): a list of Documents with metadata
197
+ from specific columns.
198
+ """
199
+ return list(self.lazy_load())
200
+
201
+ def lazy_load(self) -> Iterator[Document]:
202
+ """
203
+ A lazy loader for langchain documents from a Spanner database. Use lazy load to avoid
204
+ caching all documents in memory at once.
205
+
206
+ Returns:
207
+ (Iterator[langchain_core.documents.Document]): a list of Documents with metadata
208
+ from specific columns.
209
+ """
210
+ instance = self.client.instance(self.instance_id)
211
+ db = instance.database(self.database_id)
212
+ timestamp = (
213
+ self.staleness.replace(tzinfo=datetime.timezone.utc)
214
+ if type(self.staleness) is datetime.datetime
215
+ else None
216
+ )
217
+ duration = (
218
+ datetime.timedelta(seconds=self.staleness)
219
+ if type(self.staleness) is float
220
+ else None
221
+ )
222
+ snapshot = db.batch_snapshot(exact_staleness=duration, read_timestamp=timestamp)
223
+ partitions = snapshot.generate_query_batches(
224
+ sql=self.query, data_boost_enabled=self.databoost
225
+ )
226
+
227
+ for partition in partitions:
228
+ r = snapshot.process_query_batch(partition)
229
+ results = r.to_dict_list()
230
+ if len(results) == 0:
231
+ break
232
+ column_names = [f.name for f in r.fields]
233
+ content_columns = self.content_columns or [column_names[0]]
234
+ metadata_columns = self.metadata_columns or [
235
+ col for col in column_names if col not in content_columns
236
+ ]
237
+
238
+ for row in results:
239
+ yield _load_row_to_doc(
240
+ self.format,
241
+ content_columns,
242
+ metadata_columns,
243
+ self.metadata_json_column,
244
+ row,
245
+ )
246
+
247
+
248
+ class SpannerDocumentSaver:
249
+ """Save docs to Google Cloud Spanner."""
250
+
251
+ def __init__(
252
+ self,
253
+ instance_id: str,
254
+ database_id: str,
255
+ table_name: str,
256
+ content_column: str = CONTENT_COL_NAME,
257
+ metadata_columns: List[str] = [],
258
+ metadata_json_column: str = METADATA_COL_NAME,
259
+ primary_key: Optional[str] = None,
260
+ client: Optional[Client] = None,
261
+ ):
262
+ """Initialize Spanner document saver.
263
+
264
+ Args:
265
+ instance_id: The Spanner instance to load data to.
266
+ database_id: The Spanner database to load data to.
267
+ table_name: The table name to load data to.
268
+ content_column: The name of the content column. Defaulted to the first column.
269
+ metadata_columns: This is for user to opt-in a selection of columns to use. Defaulted to use
270
+ all columns.
271
+ metadata_json_column: The name of the special JSON column. Defaulted to use "langchain_metadata".
272
+ client: The connection object to use. This can be used to customized project id and credentials.
273
+ """
274
+ self.instance_id = instance_id
275
+ self.database_id = database_id
276
+ self.table_name = table_name
277
+ self.content_column = content_column
278
+ self.metadata_columns = metadata_columns
279
+ self.metadata_json_column = metadata_json_column
280
+ self.primary_key = primary_key or self.content_column
281
+ self.client = client_with_user_agent(client, USER_AGENT_SAVER)
282
+
283
+ instance = self.client.instance(instance_id)
284
+ if not instance.exists():
285
+ raise Exception("Instance doesn't exist.")
286
+
287
+ database = instance.database(database_id)
288
+ if not database.exists():
289
+ raise Exception("Database doesn't exist.")
290
+
291
+ database.reload()
292
+ self.dialect = database.database_dialect
293
+
294
+ table = database.table(table_name)
295
+ if not table.exists():
296
+ raise Exception(
297
+ "Table doesn't exist. Create table with SpannerDocumentSaver.init_document_table function."
298
+ )
299
+
300
+ self._table_fields = [self.primary_key]
301
+ for n in table.schema:
302
+ if n.name != metadata_json_column and n.name != self.primary_key:
303
+ self._table_fields.append(n.name)
304
+ self._table_fields.append(metadata_json_column)
305
+
306
+ def add_documents(self, documents: List[Document]):
307
+ """Add documents to the Spanner table."""
308
+ db = self.client.instance(self.instance_id).database(self.database_id)
309
+ values = [
310
+ _load_doc_to_row(
311
+ self._table_fields, doc, self.content_column, self.metadata_json_column
312
+ )
313
+ for doc in documents
314
+ ]
315
+
316
+ for values_batch in _batch(values, MUTATION_BATCH_SIZE):
317
+ with db.batch() as batch:
318
+ batch.insert(
319
+ table=self.table_name,
320
+ columns=self._table_fields,
321
+ values=values_batch,
322
+ )
323
+
324
+ def delete(self, documents: List[Document]):
325
+ """Delete documents from the table."""
326
+ database = self.client.instance(self.instance_id).database(self.database_id)
327
+ # load documents to row
328
+ docs = [
329
+ _load_doc_to_row(
330
+ self._table_fields,
331
+ doc,
332
+ self.content_column,
333
+ self.metadata_json_column,
334
+ False,
335
+ )
336
+ for doc in documents
337
+ ]
338
+ keys = [[doc[0]] for doc in docs]
339
+ docs_keys = KeySet(keys=keys)
340
+ snapshot = database.batch_snapshot()
341
+ partitions = snapshot.generate_read_batches(
342
+ table=self.table_name,
343
+ columns=tuple(self._table_fields),
344
+ keyset=docs_keys,
345
+ partition_size_bytes=5000000,
346
+ )
347
+
348
+ for partition in partitions:
349
+ keys_to_delete = []
350
+ for row in snapshot.process_read_batch(partition):
351
+ # compare whole document
352
+ if tuple(row) in docs:
353
+ keys_to_delete.append([row[0]])
354
+ with database.batch() as batch:
355
+ docs_to_delete = KeySet(keys=keys_to_delete)
356
+ batch.delete(self.table_name, docs_to_delete)
357
+
358
+ @staticmethod
359
+ def init_document_table(
360
+ instance_id: str,
361
+ database_id: str,
362
+ table_name: str,
363
+ content_column: str = CONTENT_COL_NAME,
364
+ metadata_columns: List[Column] = [],
365
+ primary_key: str = "",
366
+ store_metadata: bool = True,
367
+ metadata_json_column: str = METADATA_COL_NAME,
368
+ ):
369
+ """
370
+ Create a new table to store docs with a custom schema.
371
+
372
+ Args:
373
+ instance_id: The Spanner instance to load data to.
374
+ database_id: The Spanner database to load data to.
375
+ table_name: The table name to load data to.
376
+ content_column: The name of the content column.
377
+ metadata_columns: The metadata columns for custom schema.
378
+ primary_key: The name of the primary key.
379
+ store_metadata: If true, extra metadata will be stored in the "langchain_metadata" column.
380
+ Defaulted to true.
381
+ metadata_json_column: The name of the special JSON column. Defaulted to use "langchain_metadata".
382
+ """
383
+ client = Client()
384
+ primary_key = primary_key or content_column
385
+ metadata_json_column = metadata_json_column if store_metadata else ""
386
+
387
+ instance = client.instance(instance_id)
388
+ if not instance.exists():
389
+ raise Exception("Instance doesn't exist.")
390
+
391
+ database = instance.database(database_id)
392
+ if not database.exists():
393
+ raise Exception("Database doesn't exist.")
394
+
395
+ # create table with custom schema
396
+ SpannerDocumentSaver.create_table(
397
+ client,
398
+ instance_id,
399
+ database_id,
400
+ table_name,
401
+ primary_key,
402
+ metadata_json_column,
403
+ content_column,
404
+ metadata_columns,
405
+ )
406
+
407
+ @staticmethod
408
+ def create_table(
409
+ client: Client,
410
+ instance_id: str,
411
+ database_id: str,
412
+ table_name: str,
413
+ primary_key: str,
414
+ metadata_json_column: str,
415
+ content_column: str,
416
+ metadata_columns: List[Column],
417
+ ):
418
+ """
419
+ Create a new table in Spanner database.
420
+
421
+ Args:
422
+ client: The connection object to use.
423
+ instance_id: The Spanner instance to load data to.
424
+ database_id: The Spanner database to load data to.
425
+ table_name: The table name to load data to.
426
+ primary_key: The name of the primary key for the table.
427
+ metadata_json_column: The name of the special JSON column.
428
+ content_column: The name of the content column.
429
+ metadata_columns: The metadata columns for custom schema.
430
+ """
431
+ database = client.instance(instance_id).database(database_id)
432
+ database.reload()
433
+ dialect = database.database_dialect
434
+
435
+ ddl = f"CREATE TABLE {table_name} ("
436
+ if dialect == DatabaseDialect.POSTGRESQL:
437
+ ddl += f"{content_column} VARCHAR(1024) NOT NULL,"
438
+ for col in metadata_columns:
439
+ null_string = " NOT NULL" if col.nullable else ""
440
+ ddl += f"{col.name} {col.data_type}{null_string},"
441
+ if metadata_json_column:
442
+ ddl += f"{metadata_json_column} JSONb NOT NULL,"
443
+ ddl += f"PRIMARY KEY ({primary_key}));"
444
+ else:
445
+ ddl += f"{content_column} STRING(1024) NOT NULL,"
446
+ for col in metadata_columns:
447
+ null_string = " NOT NULL" if col.nullable else ""
448
+ ddl += f"{col.name} {col.data_type}{null_string},"
449
+ if metadata_json_column:
450
+ ddl += f"{metadata_json_column} JSON NOT NULL,"
451
+ ddl += f") PRIMARY KEY ({primary_key})"
452
+
453
+ operation = database.update_ddl([ddl])
454
+ operation.result(OPERATION_TIMEOUT_SECONDS)
File without changes