how to add an empty or data map in dynamodb? - amazon-dynamodb

I have something like this:
{
Records:{}
}
and I just want to add data inside records like this:
{
Id: 123,
Records:{
10001: { event : "item1" }
}
}
My 1st attempt:
var params = {
TableName : "Records",
Key : { Id : 123 },
UpdateExpression: 'set Record.#k1.event = :v1',
ExpressionAttributeNames: { '#k1' : 10001},
ExpressionAttributeValues: { ':v1' : 'item1' }
};
Because Record.10001 doesn't exist, it give me error:
The document path provided in the update expression is invalid for update
"ADD" only support NUMBER and SET type so its not suitable in my case.
My 2nd attempt (trying to create a empty map 1st):
var params = {
TableName : "Records",
Key : { Id : 123 },
UpdateExpression: 'set Record.#k1 = {}',
ExpressionAttributeNames: { '#k1' : 10001},
};
It fails as my syntax is probably incorrect and I cannot find how to create a empty map to an attribute.
So my question is how do I add structured object as a map or empty map to an existing item like the example above?
I can easily modifying the data from the amazon DynamoDB data management page so at least I know there exist a way to do it. I just need to know how.
Thanks

I somehow successfully done it this way:
var params = {
TableName: "Records",
Key: { Id: 123 },
UpdateExpression: 'set Records.#k1 = :v1',
ExpressionAttributeNames: {'#k1': '10001',},
ExpressionAttributeValues: {':v1': { event: 'Yay~' } }
};
optionally you could use
UpdateExpression: 'set Records.#k1 = if_not_exists( Records.#k1, :v1)',
to avoid overwriting existing records.

I found a workaround for the error we get.
try:
table.update_item(
Key = {'Id' : id},
UpdateExpression = 'SET Records.#key1.#key2 = :value1',
ExpressionAttributeNames = {'#key1': '10001', '#key2' : 'event'},
ExpressionAttributeValues = {':value1': 'singing'}
)
except:
table.update_item(
Key = {'Id' : id},
UpdateExpression = 'SET Records = :value1',
ExpressionAttributeValues = {':value1': {'10001': {'event' : 'dance'}}}
)
When we try to update for the first time we get "The document path provided in the update expression is invalid for update" error. An exception will be thrown since there will be no nested JSON.
Thereafter, any updates say for example Record.10002 will not throw exception. Update function in the try block will be executed.
Please comment on this if this is the correct way of handling this kind of scenario.

one of the possible resolution to get rid of such problem: you have to have an empty object created and exist in DB in advance.
for instance,
during generation of your object (original creation in DB):
field: { }
to update this field use:
TableName: ....,
Key: {
.....
},
UpdateExpression: 'set field.#mapKey = :v',
ExpressionAttributeNames: {
'#mapKey': myName
},
ExpressionAttributeValues: {
':v': myValue
}

This can be done in c# as follows.
If you have the following json:
"invoice":{
"M":{
":number":{
"S":"XPN1052"
}":date":{
"S":"02-17-2022"
}
}
}
You can use the following code to insert an invoice record:
ExpressionAttributeValues = new Dictionary<string, AttributeValue> {
{":invoice", new AttributeValue { M = new Dictionary<string, AttributeValue>{
{":date", new AttributeValue { S = invoice.TxnDate.ToString("MM-dd-yyyy") }},
{":number", new AttributeValue{ S = invoice.DocNumber } } }
} }},
UpdateExpression = "SET invoice = :invoice",

var params = {
TableName : "Records",
Key : { Id : 123 },
UpdateExpression: 'set #Records = :obj',
ExpressionAttributeNames: { '#Records' : 'Records'},
ExpressionAttributeValues: { ':obj' : {'10001': { 'event' : 'item1' }}}
};

In order to insert/update Dynamodb items dynamically based on a given dictionary with various keys (eventually columns in Dynamodb) we can construct query string as shown below:
# given dictionary with needs to be dumped into dynamodb
dict_to_insert = {'column1':'value1',
'column2.subdocument1':{'subkey1':'subvalue1',
'subkey2':'subvalue2'}...}...
# placeholder variables
# Using upsert for insert/update mllog records on dynamodb
query = "SET"
values_dict = {}
names_dict = {}
i = 0
# iterating over given dictionary and construct the query
for key, value in dict_to_insert.items():
# if item key column provide in the given dictionary
# then it should be skipped
if key == 'ITEM_KEY_NAME':
i += 1
continue
# None values causing error on update_item
# So, better to ignore them as well
if value is not None:
# if_not_exists method prevents overwriting existing
# values on columns
query += " " + "#k_" + str(i) + " = if_not_exists(#k_" + str(i) + \
", :v_" + str(i)+")"
if key != list(dict_to_insert.keys())[-1]: # if not the last attribute add ,
query += ","
# Defining dynamodb columns and their corresponding values
values_dict["#k_" + str(i)] = key
names_dict[":v_" + str(i)] = value
i += 1
Once we construct the query string as shown above, then we can confidently run our update_item method on Dynamodb:
# table obj here is boto3 dynamodb resource instance
table.update_item(
TableName='DYNAMODB_TABLENAME',
Key={'ITEM_KEY_NAME': 'ANY_VALUE_AS_KEY'},
UpdateExpression=query.strip(),
ExpressionAttributeNames=names_dict,
ExpressionAttributeValues=values_dict)

Related

Why do I get empty list when doing filter?

I have a table 'test-table':
id (string) - primaryKey
type (string)
I have items like this in that table, for example:
34 AWC
56 BDE
I want to do scan table and filter by type:
I use:
async getItems(typeInput) {
const params: ScanCommandInput = {
TableName: "test-table",
FilterExpression: "type in (:type)", // also tried with type = :type
ExpressionAttributeValues: { ":type": { "S": typeInput } },
};
return await dynamodbdocumentclient.send(new ScanCommand(params));
}
I get as a result empty Items. Why ?
You appear to be using the DocumentClient, which automatically marshalls attribute values from their native JavaScript type. You do not need to wrap all values in {'S': 'xxx'}, {'N': '999'}, etc. Use ExpressionAttributeValues: { ":type": typeInput }.

Why does Dynamo DB throw an error when updating a map key with the same value?

I am trying to run a simple update query, but got an error when I tried to update the key of a map to the same value. Is there a technical reason this would be disallowed? or some kind of best-practice that I am violating by trying to do this?
Error:
ValidationException: Invalid UpdateExpression: Two document paths overlap with each other;
must remove or rewrite one of these paths; path one: [questions, What is xx?], path two: [questions, What is xx?]
Query object:
{
TableName: 'notesTable',
Key: { topic: 'My tooic' },
ExpressionAttributeNames: { '#qq': 'What is xx?', '#updq': 'What is xx?' },
ExpressionAttributeValues: { ':updans': 'new answer' },
UpdateExpression: 'REMOVE questions.#qq SET questions.#updq = :updans'
}
Multiple ways to deal with scenario when same key needs to be updated. Instead of removing and updating the same key, we can simply SET the key , which replaced the value anyhow.
So, simple way is to send different updateExpression each time.
const qq = "What is xx2?";
const updq = "What is xx?";
let expressionAttributeNames;
let UpdateExpression;
if (qq === updq) {
expressionAttributeNames = { "#updq": "What is xx?" };
UpdateExpression = "SET questions.#updq = :updans";
} else {
expressionAttributeNames = { "#qq": "What is xx1?", "#updq": "What is xx?" };
UpdateExpression = "REMOVE questions.#qq SET questions.#updq = :updans";
}
docClient.update(
{
TableName: "test",
Key: {
id: "My tooic",
},
ExpressionAttributeNames: expressionAttributeNames,
ExpressionAttributeValues: { ":updans": "new answer1" },
UpdateExpression: UpdateExpression,
},
function (error, result) {
console.log("error", error, "result", result);
}
);

Meteor: Can filter a collection by _id but I can't filter a collection using other fields

I'm fairly new to meteor and I'm still trying to find my way around with filtering collections. Here is my problem, I have a collection defined as follows;
parent_id: {
label: 'Parent ID',
type: String,
},
ar_session_id: {
label: 'Session ID',
type: String,
},
I have inserted some documents and here is one;
{
"_id" : "oQdtbBtKXHzdxWvzn",
"parent_id" : "dJkbDBXut5WzwkaFN",
"ar_session_id" : "dJkbDBXut5WzwkaFNuz77MFgcuGyvgokip",
"question" : "Do you have blah blah...?",
"answer" : "no",
"createdAt" : 1564563509127
}
I am able to filter using parent_id but I can't filter using ar_session_id
var parent_id = "dJkbDBXut5WzwkaFN";
var ar_session_id = "dJkbDBXut5WzwkaFNuz77MFgcuGyvgokip";
qry1 = AssessmentResponse.find({parent_id: parent_id}).fetch();
qry2 = AssessmentResponse.find({ar_session_id: ar_session_id}).fetch();
qry2 returns an empty set. What is it that I am missing?
The only reason I could think of would be if you are not publishing ar_session_id in the client.
For instance if you had something like this:
Meteor.publish("AssessmentResponse", function () {
return AssessmentResponse.find({}, { fields: { ar_session_id: 0 } });
});
Otherwise there is no reason the filtering would be empty, assuming you don't have any typo.

Dynamo Db with Latest data

I am having parition key as id that is VIN(vehicle identification number) sort key as timestamp, I want to execute query and want to know the latest record having the maximum timestamp(only single record based on vin with latest timestamp).
You can use Query API with ScanIndexForward as false and Limit equal to 1 to achieve the result.
ScanIndexForward = false -> means arranging the sort key in descending
order
Limit = 1 -> means return only one item
Sample code:-
var params = {
TableName : "yourtablename",
KeyConditionExpression: "#VIN = :vinvalue",
ExpressionAttributeNames:{
"#VIN": "VIN"
},
ExpressionAttributeValues: {
":vinvalue":"somevinvalue"
},
ScanIndexForward : false,
Limit : 1
};
docClient.query(params, function(err, data) {
if (err) {
console.error("Unable to query. Error:", JSON.stringify(err, null, 2));
} else {
console.log("Query succeeded.");
data.Items.forEach(function(item) {
console.log(" -", item.year + ": " + item.title);
});
}
});
public EventLogEntity fetch(String vin) {
EventLogEntity eventLogEntityResult = null;
EventLogEntity entity = new EventLogEntity();
entity.setId(vin);
DynamoDBQueryExpression<EventLogEntity> queryExpression =
new DynamoDBQueryExpression<EventLogEntity>().withHashKeyValues(entity);
//DynamoDBQueryExpression Works only for Partition, Sort and Indexes in Dynamo Db
queryExpression.setScanIndexForward(false); // Will give you result in descending order
queryExpression.withLimit(1);// Will always give you single record
List<EventLogEntity> result = dynamoDBMapper.queryPage(EventLogEntity.class, queryExpression).getResults();
if (result != null && !result.isEmpty()) {
eventLogEntityResult = result.get(0);
}
return eventLogEntityResult;
}

How to set a DynamoDB Map property value, when the map doesn't exist yet

How do you "upsert" a property to a DynamoDB row. E.g. SET address.state = "MA" for some item, when address does not yet exist?
I feel like I'm having a chicken-and-egg problem because DynamoDB doesn't let you define a sloppy schema in advance.
If address DID already exist on that item, of type M (for Map), the internet tells me I could issue an UpdateExpression like:
SET #address.#state = :value
with #address, #state, and :value appropriately mapped to address, state, and MA, respectively.
But if the address property does not already exist, this gives an error:
'''
ValidationException: The document path provided in the update expression is invalid for update
'''
So.. it appears I either need to:
Figure out a way to "upsert" address.state (e.g., SET address = {}; SET address.state = 'MA' in a single command)
or
Issue three (!!!) roundtrips in which I try it, SET address = {}; on failure, and then try it again.
If the latter.... how do I set a blank map?!?
Ugh.. I like Dynamo, but unless I'm missing something obvious this is a bit crazy..
You can do it with two round trips, the first conditionally sets an empty map for address if it doesn't already exist, and the second sets the state:
db.update({
UpdateExpression: 'SET #a = :value',
ConditionExpression: 'attribute_not_exists(#a)',
ExpressionAttributeValues: {
":value": {},
},
ExpressionAttributeNames: {
'#a': 'address'
}
}, ...);
Then:
db.update({
UpdateExpression: 'SET #a.#b = :v',
ExpressionAttributeNames: {
'#a': 'address',
'#b': 'state'
},
ExpressionAttributeValues: {
':v': 'whatever'
}
}, ...);
You cannot set nested attributes if the parent document does not exist. Since address does not exist you cannot set the attribute province inside it. You can achieve your goal if you set address to an empty map when you create the item. Then, you can use the following parameters to condition an update on an attribute address.province not existing yet.
var params = {
TableName: 'Image',
Key: {
Id: 'dynamodb.png'
},
UpdateExpression: 'SET address.province = :ma',
ConditionExpression: 'attribute_not_exists(address.province)',
ExpressionAttributeValues: {
':ma': 'MA'
},
ReturnValues: 'ALL_NEW'
};
docClient.update(params, function(err, data) {
if (err) ppJson(err); // an error occurred
else ppJson(data); // successful response
});
By the way, I had to replace state with province as state is a reserved word.
Another totally different method is to simply create the address node when creating the parent document in the first place. For example assuming you have a hash key of id, you might do:
db.put({
Item: {
id: 42,
address: {}
}
}, ...);
This will allow you to simply set the address.state value as the address map already exists:
db.update({
UpdateExpression: 'SET #a.#b = :v',
AttributeExpressionNames: {
'#a': 'address',
'#b': 'state'
},
AttributeExpressionValues: {
':v': 'whatever'
}
}, ...);
Some kotlin code to do this recursively regardless how deep it goes. It sets existence of parent paths as condition and if condition check fails, recursively creates those paths first. It has to be in the library's package so it can access those package private fields/classes.
package com.amazonaws.services.dynamodbv2.xspec
import com.amazonaws.services.dynamodbv2.document.Table
import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException
import com.amazonaws.services.dynamodbv2.xspec.ExpressionSpecBuilder.attribute_exists
fun Table.updateItemByPaths(hashKeyName: String, hashKeyValue: Any, updateActions: List<UpdateAction>) {
val parentPaths = updateActions.map { it.pathOperand.path.parent() }
.filter { it.isNotEmpty() }
.toSet() // to remove duplicates
try {
val builder = ExpressionSpecBuilder()
updateActions.forEach { builder.addUpdate(it) }
if (parentPaths.isNotEmpty()) {
var condition: Condition = ComparatorCondition("=", LiteralOperand(true), LiteralOperand(true))
parentPaths.forEach { condition = condition.and(attribute_exists<Any>(it)) }
builder.withCondition(condition)
}
this.updateItem(hashKeyName, hashKeyValue, builder.buildForUpdate())
} catch (e: ConditionalCheckFailedException) {
this.updateItemByPaths(hashKeyName, hashKeyValue, parentPaths.map { M(it).set(mapOf<String, Any>()) })
this.updateItemByPaths(hashKeyName, hashKeyValue, updateActions)
}
}
private fun String.parent() = this.substringBeforeLast('.', "")
Here is a helper function I wrote in Typescript that works for this a single level of nesting using a recursive method.
I refer to the top-level attribute as a column.
//usage
await setKeyInColumn('customerA', 'address', 'state', "MA")
// Updates a map value to hold a new key value pair. It will create a top-level address if it doesn't exist.
static async setKeyInColumn(primaryKeyValue: string, colName: string, key: string, value: any, _doNotCreateColumn?:boolean) {
const obj = {};
obj[key] = value; // creates a nested value like {address:value}
// Some conditions depending on whether the column already exists or not
const ConditionExpression = _doNotCreateColumn ? undefined:`attribute_not_exists(${colName})`
const AttributeValue = _doNotCreateColumn? value : obj;
const UpdateExpression = _doNotCreateColumn? `SET ${colName}.${key} = :keyval `: `SET ${colName} = :keyval ` ;
try{
const updateParams = {
TableName: TABLE_NAME,
Key: {key:primaryKeyValue},
UpdateExpression,
ExpressionAttributeValues: {
":keyval": AttributeValue
},
ConditionExpression,
ReturnValues: "ALL_NEW",
}
const resp = await docClient.update(updateParams).promise()
if (resp && resp[colName]) {
return resp[colName];
}
}catch(ex){
//if the column already exists, then rerun and do not create it
if(ex.code === 'ConditionalCheckFailedException'){
return this.setKeyInColumn(primaryKeyValue,colName,key, value, true)
}
console.log("Failed to Update Column in DynamoDB")
console.log(ex);
return undefined
}
}
I've got quite similar situation. I can think of only a one way to do this in 1 query/atomically.
Extract map values to top level attributes.
Example
Given I have this post item in DynamoDB:
{
"PK": "123",
"SK": "post",
"title": "Hello World!"
}
And I want to later add an analytics entry to same partition:
{
"PK": "123",
"SK": "analytics#december",
"views": {
// <day of month>: <views>
"1": "12",
"2": "457463",
// etc
}
}
Like in your case, it's not possible to increment/decrement views days counters in single query if analytics item nor views map might not exist (could be later feature or don't want to put empty items).
Proposed solution:
{
"PK": "123",
"SK": "analytics#december",
// <day of month>: <views>
"1": "12", // or "day1" if "1" seems too generic
"2": "457463",
// etc
}
}
Then you could do something like this (increment +1 example):
{
UpdateExpression: "SET #day = if_not_exists(#day, 0) + 1",
AttributeExpressionNames: {
'#day': "1"
}
}
if day attribute value doesn't exist, set default value to 0
if item in database doesn't exist, update API adds a new one

Resources