Syntax

Platform.Function.UpsertData(deName, whereFieldNames, whereFieldValues, fieldNames, fieldValues)
5 arguments

Parameters

UpsertData takes exactly these five array-based arguments:

Name Type Required Description
deName string Yes Data Extension Name (the external key / CustomerKey is not accepted — runtime-verified)
whereFieldNames string[] Yes Nonempty array of column names used to find existing rows; multiple columns use positional AND logic
whereFieldValues array Yes Nonempty array of values positionally aligned to whereFieldNames
fieldNames string[] Yes Array of column names to insert or update
fieldValues array Yes Array of values aligned to fieldNames

Arrays are required. Scalar strings are rejected even for a single filter or field. The name/value arrays must be nonempty and have matching lengths.

Show test script
<script runat="server">
/*
 * Chapter: Parameters
 * Proves:
 * 1. Exactly five arguments are accepted; every other arity throws.
 * 2. All four filter/field name/value arguments require nonempty, positionally
 *    aligned arrays — scalar strings throw (the official reference allows them).
 * 3. Multiple filter columns use AND logic and multiple field values align by position.
 * 4. The DE resolves by Name only; CustomerKey and unknown identifiers throw.
 * 5. Text, Number, Decimal, Boolean and Date columns accept native values.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function addField(de, name, type, len, scale, isKey, isRequired) {
    var field = Platform.Function.CreateObject("DataExtensionField");
    Platform.Function.SetObjectProperty(field, "Name", name);
    Platform.Function.SetObjectProperty(field, "FieldType", type);
    if (len) { Platform.Function.SetObjectProperty(field, "MaxLength", len); }
    if (scale) { Platform.Function.SetObjectProperty(field, "Scale", scale); }
    Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey ? "true" : "false");
    Platform.Function.SetObjectProperty(field, "IsRequired", isRequired ? "true" : "false");
    Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
    var de = Platform.Function.CreateObject("DataExtension");
    Platform.Function.SetObjectProperty(de, "CustomerKey", key);
    Platform.Function.SetObjectProperty(de, "Name", name);
    addField(de, "Id", "Text", "50", null, true, true);
    addField(de, "Grp", "Text", "50", null, false, false);
    addField(de, "Txt", "Text", "100", null, false, false);
    addField(de, "Num", "Number", null, null, false, false);
    addField(de, "Dec", "Decimal", "18", "2", false, false);
    addField(de, "Flag", "Boolean", null, null, false, false);
    addField(de, "Dt", "Date", null, null, false, false);
    var status = [0, 0];
    return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
    var de = Platform.Function.CreateObject("DataExtension");
    Platform.Function.SetObjectProperty(de, "CustomerKey", key);
    var status = [0, 0];
    return String(Platform.Function.InvokeDelete(de, status, null));
}
function countRows(name, field, value) {
    var rows = Platform.Function.LookupRows(name, field, value);
    return rows === null ? 0 : rows.length;
}
var deName = "ssjsg_up_par_2334_name", deKey = "ssjsg_up_par_2334_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
assert("control: Name and CustomerKey differ", deName === deKey ? "same" : "different", "different");
var inserted = Platform.Function.UpsertData(deName, ["Id", "Grp"], ["typed", "group-a"], ["Txt", "Num", "Dec", "Flag", "Dt"], ["value", 42, 3.14, true, new Date(2024, 0, 15)]);
assert("the five-argument array call inserts one row", inserted, 1);
assert("the return value has typeof number", String(typeof inserted), "number");
assert("setup: a second row shares the Grp filter value", Platform.Function.InsertData(deName, ["Id", "Grp", "Txt"], ["sibling", "group-a", "seed"]), 1);
assert("one filter column alone matches both rows", Platform.Function.UpsertData(deName, ["Grp"], ["group-a"], ["Txt"], ["broad"]), 2);
assert("adding a second filter column narrows the match with AND logic", Platform.Function.UpsertData(deName, ["Grp", "Id"], ["group-a", "typed"], ["Txt", "Num"], ["updated", 42]), 1);
assert("the aligned multi-field update persisted its first value", String(Platform.Function.Lookup(deName, "Txt", "Id", "typed")), "updated");
assert("the AND filter left the sibling row untouched", String(Platform.Function.Lookup(deName, "Txt", "Id", "sibling")), "broad");
assert("Number columns persist as numbers", String(typeof Platform.Function.Lookup(deName, "Num", "Txt", "updated")), "number");
assert("Decimal columns persist as numbers", String(typeof Platform.Function.Lookup(deName, "Dec", "Num", 42)), "number");
assert("Boolean columns persist as booleans", String(typeof Platform.Function.Lookup(deName, "Flag", "Dec", 3.14)), "boolean");
assert("Date columns persist as Date-like objects", String(typeof Platform.Function.Lookup(deName, "Dt", "Flag", true)), "object");
assertThrows("DEV scalar whereFieldNames throws (docs: string or string[])", function () { return Platform.Function.UpsertData(deName, "Id", ["typed"], ["Txt"], ["x"]); });
assertThrows("DEV scalar whereFieldValues throws (docs: string or array)", function () { return Platform.Function.UpsertData(deName, ["Id"], "typed", ["Txt"], ["x"]); });
assertThrows("scalar fieldNames throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["typed"], "Txt", ["x"]); });
assertThrows("scalar fieldValues throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["typed"], ["Txt"], "x"); });
assertThrows("empty filter arrays throw", function () { return Platform.Function.UpsertData(deName, [], [], ["Txt"], ["x"]); });
assertThrows("empty field arrays throw", function () { return Platform.Function.UpsertData(deName, ["Id"], ["typed"], [], []); });
assertThrows("more filter names than values throws", function () { return Platform.Function.UpsertData(deName, ["Id", "Grp"], ["typed"], ["Txt"], ["x"]); });
assertThrows("more filter values than names throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["typed", "group-b"], ["Txt"], ["x"]); });
assertThrows("more field names than values throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["typed"], ["Txt", "Grp"], ["x"]); });
assertThrows("more field values than names throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["typed"], ["Txt"], ["x", "y"]); });
assertThrows("arity 0 throws", function () { return Platform.Function.UpsertData(); });
assertThrows("arity 1 throws", function () { return Platform.Function.UpsertData(deName); });
assertThrows("arity 2 throws", function () { return Platform.Function.UpsertData(deName, ["Id"]); });
assertThrows("arity 3 throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["typed"]); });
assertThrows("arity 4 throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["typed"], ["Txt"]); });
assertThrows("arity 6 throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["typed"], ["Txt"], ["x"], "extra"); });
assertThrows("DEV the CustomerKey form throws (docs: data extension name)", function () { return Platform.Function.UpsertData(deKey, ["Id"], ["key"], ["Txt"], ["x"]); });
assertThrows("an unknown DE throws", function () { return Platform.Function.UpsertData("ssjsg_up_no_such_de", ["Id"], ["x"], ["Txt"], ["x"]); });
assertThrows("an unknown filter column throws", function () { return Platform.Function.UpsertData(deName, ["NoSuchFilter"], ["x"], ["Txt"], ["x"]); });
assertThrows("an unknown field column throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["typed"], ["NoSuchField"], ["x"]); });
assert("none of the rejected calls added a row", countRows(deName, "Id", "typed"), 1);
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>

Description

UpsertData checks for rows matching all supplied filter pairs:

  • If there are no matches, it inserts one row using both the filter pairs and field pairs.
  • If there is one match, it updates that row in place.
  • If there are multiple matches, it updates all of them.

The return value is a number: 1 for a new insert, 1 for one updated match, or the affected-row count for multiple matches.

Show test script — array-only signature
<script runat="server">
/*
 * Differs-from-docs claim: the official reference permits scalar strings for a
 * single whereFieldNames / whereFieldValues pair, and the runtime rejects them.
 * Proves:
 * 1. Each of the four name/value positions throws when given a scalar string.
 * 2. The recommended workaround — one-element arrays — inserts and updates.
 * 3. The DE is resolved by Name; the CustomerKey form throws.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function addField(de, name, len, isKey) {
    var field = Platform.Function.CreateObject("DataExtensionField");
    Platform.Function.SetObjectProperty(field, "Name", name);
    Platform.Function.SetObjectProperty(field, "FieldType", "Text");
    Platform.Function.SetObjectProperty(field, "MaxLength", len);
    Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey ? "true" : "false");
    Platform.Function.SetObjectProperty(field, "IsRequired", isKey ? "true" : "false");
    Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
    var de = Platform.Function.CreateObject("DataExtension");
    Platform.Function.SetObjectProperty(de, "CustomerKey", key);
    Platform.Function.SetObjectProperty(de, "Name", name);
    addField(de, "Id", "50", true);
    addField(de, "Txt", "100", false);
    var status = [0, 0];
    return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
    var de = Platform.Function.CreateObject("DataExtension");
    Platform.Function.SetObjectProperty(de, "CustomerKey", key);
    var status = [0, 0];
    return String(Platform.Function.InvokeDelete(de, status, null));
}
var deName = "ssjsg_up_arr_2247_name", deKey = "ssjsg_up_arr_2247_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
assert("control: Name and CustomerKey differ", deName === deKey ? "same" : "different", "different");
assertThrows("DEV scalar whereFieldNames throws (docs: string or string[])", function () { return Platform.Function.UpsertData(deName, "Id", ["a"], ["Txt"], ["wrong"]); });
assertThrows("DEV scalar whereFieldValues throws (docs: string or array)", function () { return Platform.Function.UpsertData(deName, ["Id"], "a", ["Txt"], ["wrong"]); });
assertThrows("scalar fieldNames throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["a"], "Txt", ["wrong"]); });
assertThrows("scalar fieldValues throws", function () { return Platform.Function.UpsertData(deName, ["Id"], ["a"], ["Txt"], "wrong"); });
assertThrows("DEV the CustomerKey form throws (docs: data extension name)", function () { return Platform.Function.UpsertData(deKey, ["Id"], ["a"], ["Txt"], ["wrong"]); });
assert("workaround: one-element arrays insert a new row", Platform.Function.UpsertData(deName, ["Id"], ["a"], ["Txt"], ["inserted"]), 1);
assert("workaround: one-element arrays update the existing row", Platform.Function.UpsertData(deName, ["Id"], ["a"], ["Txt"], ["updated"]), 1);
assert("the workaround update committed", String(Platform.Function.Lookup(deName, "Id", "Txt", "updated")), "a");
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>

This is the most robust write operation for Data Extensions — use it instead of InsertData when you’re not sure if the row already exists.

Show test script
<script runat="server">
/*
 * Chapter: Description
 * Proves the three documented branches independently:
 * 1. No match  -> one row is inserted from the filter pairs AND the field pairs; returns 1.
 * 2. One match -> that row is updated in place, the row count stays 1; returns 1.
 * 3. Many matches -> every match is updated; returns the affected-row count (2).
 * Also proves the return value is always a number.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function addField(de, name, len, isKey, isRequired) {
    var field = Platform.Function.CreateObject("DataExtensionField");
    Platform.Function.SetObjectProperty(field, "Name", name);
    Platform.Function.SetObjectProperty(field, "FieldType", "Text");
    Platform.Function.SetObjectProperty(field, "MaxLength", len);
    Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey ? "true" : "false");
    Platform.Function.SetObjectProperty(field, "IsRequired", isRequired ? "true" : "false");
    Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
    var de = Platform.Function.CreateObject("DataExtension");
    Platform.Function.SetObjectProperty(de, "CustomerKey", key);
    Platform.Function.SetObjectProperty(de, "Name", name);
    addField(de, "Id", "50", true, true);
    addField(de, "Grp", "50", false, false);
    addField(de, "Txt", "100", false, false);
    addField(de, "Req", "50", false, true);
    var status = [0, 0];
    return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
    var de = Platform.Function.CreateObject("DataExtension");
    Platform.Function.SetObjectProperty(de, "CustomerKey", key);
    var status = [0, 0];
    return String(Platform.Function.InvokeDelete(de, status, null));
}
function countRows(name, field, value) {
    var rows = Platform.Function.LookupRows(name, field, value);
    return rows === null ? 0 : rows.length;
}
var deName = "ssjsg_up_dsc_2247_name", deKey = "ssjsg_up_dsc_2247_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
var insertResult = Platform.Function.UpsertData(deName, ["Id", "Grp"], ["new", "insert"], ["Txt", "Req"], ["inserted", "required"]);
assert("no match inserts exactly one row", insertResult, 1);
assert("the insert return has typeof number", String(typeof insertResult), "number");
assert("the filter pairs became part of the inserted row", String(Platform.Function.Lookup(deName, "Grp", "Id", "new")), "insert");
assert("every supplied field pair persisted on the inserted row", String(Platform.Function.Lookup(deName, "Req", "Txt", "inserted")), "required");
var updateResult = Platform.Function.UpsertData(deName, ["Id"], ["new"], ["Txt"], ["updated"]);
assert("one match updates exactly one row", updateResult, 1);
assert("the one-match return has typeof number", String(typeof updateResult), "number");
assert("the one-match branch keeps the row count at one", countRows(deName, "Id", "new"), 1);
assert("the one-match branch updated the row in place", String(Platform.Function.Lookup(deName, "Id", "Txt", "updated")), "new");
assert("setup: first multi-match row inserted", Platform.Function.InsertData(deName, ["Id", "Grp", "Txt", "Req"], ["many-1", "multi", "seed", "required"]), 1);
assert("setup: second multi-match row inserted", Platform.Function.InsertData(deName, ["Id", "Grp", "Txt", "Req"], ["many-2", "multi", "seed", "required"]), 1);
var manyResult = Platform.Function.UpsertData(deName, ["Grp"], ["multi"], ["Txt"], ["many-updated"]);
assert("multiple matches return the affected-row count", manyResult, 2);
assert("the multiple-match return has typeof number", String(typeof manyResult), "number");
assert("every matching row was updated", countRows(deName, "Txt", "many-updated"), 2);
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>

Examples

Standard array syntax (preferred)

var rowsAffected = Platform.Function.UpsertData(
    "Subscribers",
    ["SubscriberKey"],              // whereFieldNames (filter/identity)
    [subscriberKey],                // whereFieldValues
    ["Email", "FirstName", "City"], // fieldNames (data columns)
    [email, firstName, city]        // fieldValues
);

Multiple primary keys

Platform.Function.UpsertData(
    "OrderItems",
    ["OrderID", "ProductSKU"],   // whereFieldNames: composite key
    [orderId, sku],              // whereFieldValues
    ["Quantity", "Price"],       // fieldNames
    [qty, price]                 // fieldValues
);

Single column upsert

// Even a single column/key must be passed as arrays
Platform.Function.UpsertData(
    "PageViews",
    ["PageID"], [pageId],     // whereFieldNames / whereFieldValues (key)
    ["Count"],  [viewCount]   // fieldNames / fieldValues (column to set)
);

Track login with upsert

Platform.Function.UpsertData(
    "UserActivity",
    ["SubscriberKey"],
    [sk],
    ["LastLogin", "LoginCount", "Status"],
    [Platform.Function.Now(), loginCount, "active"]
);

Error handling

try {
    Platform.Function.UpsertData(
        "Registrations",
        ["Email"],
        [email],
        ["Name", "RegisteredAt"],
        [name,   Platform.Function.Now()]
    );
    Platform.Response.Redirect("/confirmation", false);
} catch (e) {
    Write("Save failed: " + e.message);
}
Show test script
<script runat="server">
/*
 * Chapter: Examples
 * Proves every example shape on the page:
 * 1. Standard array syntax — one filter column, three data columns.
 * 2. Multiple primary keys — a composite filter inserts, then updates the same row.
 * 3. Single column upsert — one-element arrays in all four positions.
 * 4. Track login with upsert — Platform.Function.Now(), a Number and a Text together.
 * 5. Error handling — a failing upsert is a catchable exception with a message.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function addField(de, name, type, len, isKey) {
    var field = Platform.Function.CreateObject("DataExtensionField");
    Platform.Function.SetObjectProperty(field, "Name", name);
    Platform.Function.SetObjectProperty(field, "FieldType", type);
    if (len) { Platform.Function.SetObjectProperty(field, "MaxLength", len); }
    Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey ? "true" : "false");
    Platform.Function.SetObjectProperty(field, "IsRequired", isKey ? "true" : "false");
    Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
    var de = Platform.Function.CreateObject("DataExtension");
    Platform.Function.SetObjectProperty(de, "CustomerKey", key);
    Platform.Function.SetObjectProperty(de, "Name", name);
    addField(de, "Id", "Text", "50", true);
    addField(de, "Email", "Text", "100", false);
    addField(de, "FirstName", "Text", "50", false);
    addField(de, "City", "Text", "50", false);
    addField(de, "OrderID", "Text", "50", false);
    addField(de, "ProductSKU", "Text", "50", false);
    addField(de, "Quantity", "Number", null, false);
    addField(de, "Count", "Number", null, false);
    addField(de, "LastLogin", "Date", null, false);
    addField(de, "Status", "Text", "50", false);
    var status = [0, 0];
    return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
    var de = Platform.Function.CreateObject("DataExtension");
    Platform.Function.SetObjectProperty(de, "CustomerKey", key);
    var status = [0, 0];
    return String(Platform.Function.InvokeDelete(de, status, null));
}
var deName = "ssjsg_up_exa_2247_name", deKey = "ssjsg_up_exa_2247_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
assert("standard array syntax inserts one row", Platform.Function.UpsertData(deName, ["Id"], ["subscriber"], ["Email", "FirstName", "City"], ["a@example.com", "Ada", "London"]), 1);
assert("the standard example persisted every data column", String(Platform.Function.Lookup(deName, "Email", "City", "London")), "a@example.com");
assert("a composite filter inserts when nothing matches", Platform.Function.UpsertData(deName, ["Id", "OrderID"], ["order-1", "ORD-1"], ["ProductSKU", "Quantity"], ["SKU-9", 2]), 1);
assert("the same composite filter then updates that one row", Platform.Function.UpsertData(deName, ["Id", "OrderID"], ["order-1", "ORD-1"], ["Quantity"], [5]), 1);
assert("the composite-key update committed", String(Platform.Function.Lookup(deName, "Quantity", "ProductSKU", "SKU-9")), "5");
assert("a single-column upsert inserts with one-element arrays", Platform.Function.UpsertData(deName, ["Id"], ["page-1"], ["Count"], [7]), 1);
assert("the single-column upsert then updates the same row", Platform.Function.UpsertData(deName, ["Id"], ["page-1"], ["Count"], [8]), 1);
assert("the single-column value committed", String(Platform.Function.Lookup(deName, "Id", "Count", 8)), "page-1");
assert("the track-login example writes a Date, a Number and a Text together", Platform.Function.UpsertData(deName, ["Id"], ["subscriber"], ["LastLogin", "Count", "Status"], [Platform.Function.Now(), 3, "active"]), 1);
assert("the track-login Date column persisted as an object", String(typeof Platform.Function.Lookup(deName, "LastLogin", "Status", "active")), "object");
var caught = "";
try {
    Platform.Function.UpsertData(deName, ["Id"], ["registration"], ["NoSuchColumn"], ["boom"]);
} catch (e) {
    caught = e.message;
}
assert("the error-handling example catches a failing upsert", caught === "" ? "not caught" : "caught", "caught");
assert("the caught error exposes a nonempty message", String(caught).length > 0 ? "nonempty" : "empty", "nonempty");
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>

Notes

  • Resolves the DE by Name, not external key / CustomerKey.
  • All four filter and field name/value parameters require nonempty, equal-length arrays.
  • The filter columns do not have to be primary-key columns. If they match several rows, every match is updated; if they match none, their values become part of the inserted row.
  • Missing required insert fields and primary-key conflicts throw.
  • Number, Boolean, Date, and array values coerce when written to Text fields. Explicit "", null, and undefined persist as ordinary empty strings in nullable Text.
  • DE names, column names, and Text filter values matched case-insensitively in the tested business unit.
  • Platform.Function.UpsertData works without Core; Platform.Load("core", ...) does not create a bare UpsertData global.
  • UpsertDE performs the same upsert but returns null instead of a row count. Its insert and update branches should be verified independently rather than inferred from this function.
  • For large batch upserts, consider WSProxy’s updateBatch for better performance.
Show test script
<script runat="server">
/*
 * Chapter: Notes
 * Proves:
 * 1. The DE resolves by Name; the CustomerKey form throws.
 * 2. Filter columns need not be primary keys — a non-key filter updates every match.
 * 3. Missing required insert fields and primary-key conflicts throw.
 * 4. Number, Boolean, Date and array values coerce into Text columns.
 * 5. Explicit "", null and undefined persist as ordinary empty strings in nullable Text.
 * 6. DE names, column names and Text filter values match case-insensitively in this BU.
 * 7. Platform.Function.UpsertData works before Platform.Load("core", …), and Core does
 *    not create a callable bare UpsertData global.
 * 8. UpsertDE performs the same upsert but returns null instead of a row count.
 * NON-ASSERTIONS (documented, not deterministically observable from this harness):
 *  - email, automation and triggered-send execution contexts — the harness is a CloudPage.
 *  - the WSProxy updateBatch performance recommendation for large batches — a
 *    throughput claim, not an observable return value.
 * EXPECTED OUTPUT: every line starts with PASS.
 */
function assert(id, actual, expected) {
    Platform.Response.Write((actual === expected ? "PASS " : "FAIL ") + id + " -> [" + actual + "]\n");
}
function assertThrows(id, fn) {
    var threw = false, msg = "";
    try { fn(); } catch (ex) { threw = true; msg = ex.message; }
    Platform.Response.Write((threw ? "PASS " : "FAIL ") + id + " -> " + (threw ? "threw: " + msg : "did NOT throw") + "\n");
}
function addField(de, name, len, isKey) {
    var field = Platform.Function.CreateObject("DataExtensionField");
    Platform.Function.SetObjectProperty(field, "Name", name);
    Platform.Function.SetObjectProperty(field, "FieldType", "Text");
    Platform.Function.SetObjectProperty(field, "MaxLength", len);
    Platform.Function.SetObjectProperty(field, "IsPrimaryKey", isKey ? "true" : "false");
    Platform.Function.SetObjectProperty(field, "IsRequired", isKey ? "true" : "false");
    Platform.Function.AddObjectArrayItem(de, "Fields", field);
}
function createDE(name, key) {
    var de = Platform.Function.CreateObject("DataExtension");
    Platform.Function.SetObjectProperty(de, "CustomerKey", key);
    Platform.Function.SetObjectProperty(de, "Name", name);
    addField(de, "Id", "50", true);
    addField(de, "Grp", "50", false);
    addField(de, "Txt", "100", false);
    var status = [0, 0];
    return String(Platform.Function.InvokeCreate(de, status, null));
}
function dropDE(key) {
    var de = Platform.Function.CreateObject("DataExtension");
    Platform.Function.SetObjectProperty(de, "CustomerKey", key);
    var status = [0, 0];
    return String(Platform.Function.InvokeDelete(de, status, null));
}
function countRows(name, field, value) {
    var rows = Platform.Function.LookupRows(name, field, value);
    return rows === null ? 0 : rows.length;
}
var deName = "ssjsg_up_not_2247_name", deKey = "ssjsg_up_not_2247_key";
assert("setup: throwaway DE created", createDE(deName, deKey), "OK");
assert("the qualified call works before Platform.Load(\"core\")", Platform.Function.UpsertData(deName, ["Id"], ["before"], ["Grp", "Txt"], ["control", "before-ok"]), 1);
assertThrows("a bare UpsertData call throws before Core is loaded", function () { return UpsertData(deName, ["Id"], ["bare-before"], ["Txt"], ["wrong"]); });
Platform.Load("core", "1.1.5");
assert("the qualified call still works after Core is loaded", Platform.Function.UpsertData(deName, ["Id"], ["after"], ["Grp", "Txt"], ["control", "after-ok"]), 1);
assert("Core does not create a bare UpsertData global", String(typeof UpsertData), "undefined");
assertThrows("a bare UpsertData call still throws after Core is loaded", function () { return UpsertData(deName, ["Id"], ["bare-after"], ["Txt"], ["wrong"]); });
assertThrows("the CustomerKey is rejected (the DE resolves by Name)", function () { return Platform.Function.UpsertData(deKey, ["Id"], ["key"], ["Txt"], ["wrong"]); });
assertThrows("an insert missing the required primary-key column throws", function () { return Platform.Function.UpsertData(deName, ["Grp"], ["ghost-one"], ["Txt"], ["wrong"]); });
assertThrows("an insert that duplicates an existing primary key throws", function () { return Platform.Function.UpsertData(deName, ["Grp"], ["ghost-two"], ["Id", "Txt"], ["before", "wrong"]); });
assert("this BU matches DE names case-insensitively", Platform.Function.UpsertData(deName.toUpperCase(), ["Id"], ["case-de"], ["Grp", "Txt"], ["case", "de-ok"]), 1);
assert("this BU matches filter columns case-insensitively", Platform.Function.UpsertData(deName, ["ID"], ["case-filter"], ["Grp", "Txt"], ["case", "filter-ok"]), 1);
assert("this BU matches field columns case-insensitively", Platform.Function.UpsertData(deName, ["Id"], ["case-field"], ["Grp", "TXT"], ["case", "field-ok"]), 1);
assert("a non-key filter matches every row and Text values are case-insensitive", Platform.Function.UpsertData(deName, ["Grp"], ["CASE"], ["Txt"], ["case-updated"]), 3);
assert("number values coerce into Text", Platform.Function.UpsertData(deName, ["Id"], ["num"], ["Grp", "Txt"], ["coerce", 123]), 1);
assert("boolean values coerce into Text", Platform.Function.UpsertData(deName, ["Id"], ["bool"], ["Grp", "Txt"], ["coerce", true]), 1);
assert("Date values coerce into Text", Platform.Function.UpsertData(deName, ["Id"], ["date"], ["Grp", "Txt"], ["coerce", new Date(2024, 0, 15)]), 1);
assert("array values coerce into Text", Platform.Function.UpsertData(deName, ["Id"], ["array"], ["Grp", "Txt"], ["coerce", ["a", "b"]]), 1);
assert("an explicit empty string writes to nullable Text", Platform.Function.UpsertData(deName, ["Id"], ["empty"], ["Grp", "Txt"], ["empty", ""]), 1);
assert("null writes to nullable Text", Platform.Function.UpsertData(deName, ["Id"], ["null"], ["Grp", "Txt"], ["null", null]), 1);
assert("undefined writes to nullable Text", Platform.Function.UpsertData(deName, ["Id"], ["undefined"], ["Grp", "Txt"], ["undefined", undefined]), 1);
assert("a number persisted into Text reads back as its digits", String(Platform.Function.Lookup(deName, "Txt", "Id", "num")), "123");
assert("a boolean persisted into Text reads back capitalised", String(Platform.Function.Lookup(deName, "Txt", "Id", "bool")), "True");
assert("an array persisted into Text reads back as the CLR list name", String(Platform.Function.Lookup(deName, "Txt", "Id", "array")), "System.Collections.ArrayList");
var emptyRows = Platform.Function.LookupRows(deName, "Grp", "empty");
assert("an explicit empty string persists with typeof string", String(typeof emptyRows[0]["Txt"]), "string");
assert("an explicit empty string persists as an empty string", String(emptyRows[0]["Txt"]), "");
var nullRows = Platform.Function.LookupRows(deName, "Grp", "null");
assert("null persists as an ordinary empty string", String(nullRows[0]["Txt"]), "");
var undefinedRows = Platform.Function.LookupRows(deName, "Grp", "undefined");
assert("undefined persists as an ordinary empty string", String(undefinedRows[0]["Txt"]), "");
var upsertDeResult = Platform.Function.UpsertDE(deName, ["Id"], ["de-form"], ["Grp", "Txt"], ["de", "de-ok"]);
assert("UpsertDE returns null instead of a row count", upsertDeResult === null ? "null" : "not null", "null");
assert("UpsertDE still committed its row", countRows(deName, "Txt", "de-ok"), 1);
assert("the pre-Core insert persisted", countRows(deName, "Txt", "before-ok"), 1);
assert("the post-Core insert persisted", countRows(deName, "Txt", "after-ok"), 1);
assert("no bare-name or rejected call wrote a row", countRows(deName, "Txt", "wrong"), 0);
assert("cleanup: throwaway DE deleted", dropDE(deKey), "OK");
</script>

See Also