Skip to content

Commit 8d8a326

Browse files
authored
Merge pull request #17 from rush-db/feat/upser-import-create-split
Upsert, create_many, import_json separation
2 parents d8d2d2b + 9478176 commit 8d8a326

File tree

2 files changed

+133
-31
lines changed

2 files changed

+133
-31
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "rushdb"
3-
version = "1.14.0"
3+
version = "1.19.0"
44
description = "RushDB Python SDK"
55
authors = [
66
{name = "RushDB Team", email = "hi@rushdb.com"}

src/rushdb/api/records.py

Lines changed: 132 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -155,43 +155,31 @@ def create(
155155
def create_many(
156156
self,
157157
label: str,
158-
data: Union[Dict[str, Any], List[Dict[str, Any]]],
159-
options: Optional[Dict[str, bool]] = None,
158+
data: List[Dict[str, Any]],
159+
options: Optional[Dict[str, Any]] = None,
160160
transaction: Optional[Transaction] = None,
161161
) -> List[Record]:
162-
"""Create multiple records in a single operation.
162+
"""Create multiple flat records in a single operation.
163+
164+
This helper maps directly to the ``/records/import/json`` endpoint and is
165+
intended for CSV-like flat rows (no nested objects or arrays). For nested
166+
or complex JSON payloads, use :meth:`import_json` instead.
163167
164-
Creates multiple records with the same label but different data.
165-
This is more efficient than creating records individually when
166-
you need to insert many records at once.
168+
The behaviour mirrors the TypeScript SDK ``records.createMany`` method,
169+
including support for upsert semantics via ``options.mergeBy`` and
170+
``options.mergeStrategy``.
167171
168172
Args:
169-
label (str): The label/type to assign to all new records.
170-
data (Union[Dict[str, Any], List[Dict[str, Any]]]): The data for the records.
171-
Can be a single dictionary or a list of dictionaries.
172-
options (Optional[Dict[str, bool]], optional): Configuration options for the operation.
173-
Available options:
174-
- returnResult (bool): Whether to return the created records data. Defaults to True.
175-
- suggestTypes (bool): Whether to automatically suggest data types. Defaults to True.
176-
transaction (Optional[Transaction], optional): Transaction context for the operation.
177-
If provided, the operation will be part of the transaction. Defaults to None.
173+
label: The label/type to assign to all new records.
174+
data: A list of flat dictionaries. Each dictionary represents a single
175+
record. Nested objects/arrays are not supported here.
176+
options: Optional write options forwarded as-is to the server
177+
(e.g. ``suggestTypes``, ``mergeBy``, ``mergeStrategy``, etc.).
178+
transaction: Optional transaction context for the operation.
178179
179180
Returns:
180-
List[Record]: A list of Record objects representing the newly created records.
181-
182-
Raises:
183-
ValueError: If the label is empty or data is invalid.
184-
RequestError: If the server request fails.
185-
186-
Example:
187-
>>> records_api = RecordsAPI(client)
188-
>>> users_data = [
189-
... {"name": "John Doe", "email": "john@example.com"},
190-
... {"name": "Jane Smith", "email": "jane@example.com"}
191-
... ]
192-
>>> new_records = records_api.create_many("User", users_data)
193-
>>> print(len(new_records))
194-
2
181+
List[Record]: A list of Record objects representing the created
182+
(or upserted) records when ``options.returnResult`` is true.
195183
"""
196184
headers = Transaction._build_transaction_header(transaction)
197185

@@ -205,6 +193,120 @@ def create_many(
205193
)
206194
return [Record(self.client, record) for record in response.get("data")]
207195

196+
def import_json(
197+
self,
198+
data: Any,
199+
label: Optional[str] = None,
200+
options: Optional[Dict[str, Any]] = None,
201+
transaction: Optional[Transaction] = None,
202+
) -> List[Record]:
203+
"""Import nested or complex JSON payloads.
204+
205+
Works in two modes:
206+
207+
- With ``label`` provided: imports ``data`` under the given label.
208+
- Without ``label``: expects ``data`` to be a mapping with a single
209+
top-level key that determines the label, e.g. ``{"ITEM": [...]}``.
210+
211+
This mirrors the behaviour of the TypeScript SDK ``records.importJson``
212+
method and is suitable for nested, mixed, or hash-map-like JSON
213+
structures.
214+
215+
Args:
216+
data: Arbitrary JSON-serialisable structure to import.
217+
label: Optional label; if omitted, inferred from a single top-level
218+
key of ``data``.
219+
options: Optional import/write options (see server docs).
220+
transaction: Optional transaction context for the operation.
221+
222+
Returns:
223+
List[Record]: Imported records when ``options.returnResult`` is true.
224+
225+
Raises:
226+
ValueError: If ``label`` is omitted and ``data`` is not an object
227+
with a single top-level key.
228+
"""
229+
230+
inferred_label = label
231+
payload_data: Any = data
232+
233+
if inferred_label is None:
234+
if isinstance(data, dict):
235+
keys = list(data.keys())
236+
if len(keys) == 1:
237+
inferred_label = keys[0]
238+
payload_data = data[inferred_label]
239+
else:
240+
raise ValueError(
241+
"records.import_json: Missing `label`. Provide `label` or "
242+
"pass an object with a single top-level key that determines "
243+
"the label, e.g. { ITEM: [...] }."
244+
)
245+
else:
246+
raise ValueError(
247+
"records.import_json: Missing `label`. Provide `label` or pass "
248+
"an object with a single top-level key that determines the "
249+
"label, e.g. { ITEM: [...] }."
250+
)
251+
252+
headers = Transaction._build_transaction_header(transaction)
253+
payload = {
254+
"label": inferred_label,
255+
"data": payload_data,
256+
"options": options or {"returnResult": True, "suggestTypes": True},
257+
}
258+
259+
response = self.client._make_request(
260+
"POST", "/records/import/json", payload, headers
261+
)
262+
return [Record(self.client, record) for record in response.get("data")]
263+
264+
def upsert(
265+
self,
266+
data: Dict[str, Any],
267+
label: Optional[str] = None,
268+
options: Optional[Dict[str, Any]] = None,
269+
transaction: Optional[Transaction] = None,
270+
) -> Record:
271+
"""Upsert a single record.
272+
273+
Attempts to find an existing record matching the provided criteria and
274+
either updates it or creates a new one. This mirrors the behaviour of the
275+
TypeScript SDK ``records.upsert`` method.
276+
277+
Args:
278+
data: A flat dictionary containing the record data.
279+
label: Optional label/type of the record.
280+
options: Optional upsert options, including ``mergeBy`` and
281+
``mergeStrategy`` as well as standard write options.
282+
transaction: Optional transaction context for the operation.
283+
284+
Returns:
285+
Record: The upserted record instance.
286+
"""
287+
288+
headers = Transaction._build_transaction_header(transaction)
289+
290+
# Ensure upsert semantics always receive a mergeBy array by default.
291+
# This mirrors the JavaScript SDK behaviour where mergeBy defaults to []
292+
# and the server interprets an empty array as "use all incoming keys".
293+
normalized_options: Dict[str, Any] = {
294+
"returnResult": True,
295+
"suggestTypes": True,
296+
"mergeBy": [],
297+
}
298+
if options:
299+
normalized_options.update(options)
300+
301+
payload: Dict[str, Any] = {
302+
"label": label,
303+
"data": data,
304+
"options": normalized_options,
305+
}
306+
307+
response = self.client._make_request("POST", "/records", payload, headers)
308+
return Record(self.client, response.get("data"))
309+
208310
def attach(
209311
self,
210312
source: Union[str, Dict[str, Any]],

0 commit comments

Comments
 (0)