Datastores: Attempts to store a mixed table / array with holes / cyclic table will silently cut invalid part and go through

DataStores appear to accept mixed (sub-)tables / array with holes / cyclic tables as requests do not throw errors or warnings for attempts to store them, but it actually cuts off the invalid parts or replaces them with garbage. This can be seen when the data is requested via GetAsync afterwards. This is bad, because it does not trigger any warning or error for the developer, so this is really hard to detect and debug for inexperienced developers.

How to reproduce:

  1. Open a baseplate and publish it to a game with Studio API access.
  2. Run this in the command bar in Studio:
local data = game:GetService("DataStoreService"):GetDataStore("Test")

data:SetAsync("TestKey1", {a = 1, 2, 3}) -- mixed table

print("mixed table after storing")
for i,v in pairs(data:GetAsync("TestKey1")) do
	print(i..": "..tostring(v))
end

print() -- empty line

data:SetAsync("TestKey2", {[1] = 1, [3] = 2, [4] = 3, [5] = 4}) -- array with hole

print("hole array after storing")
for i,v in pairs(data:GetAsync("TestKey2")) do
	print(i..": "..tostring(v))
end

print() -- empty line

data:SetAsync("TestKey3", {[0] = 1, 2, 3, 4}) -- array not starting at index 1

print("array not starting at 1 after storing")
for i,v in pairs(data:GetAsync("TestKey3")) do
	print(i..": "..tostring(v))
end

print() -- empty line

local cyclic = {}
cyclic.cyclic = cyclic
data:SetAsync("TestKey4", cyclic) -- cyclic table

print("cyclic table after storing")
for i,v in pairs(data:GetAsync("TestKey4")) do
	print(i..": "..tostring(v))
end
  1. Observe output.

Observed behavior:

The SetAsync requests will go through without errors or warnings.

However, this is printed in the output, indicating that (1) the dictionary key (a = 1) got cut off for the mixed table, (2) the indices past the hole got cut off for the array with holes, (3) indices before 1 in an array get cut off, (4) cyclic tables have their cyclic references replaced with a garbage arbitrary string(!):

mixed table after storing
1: 2
2: 3

array not starting at 1 after storing
1: 2
2: 3
3: 4

hole array after storing
1: 1

cyclic table after storing
cyclic: *** certain entries belong to the same table ***

Yes, that last one actually replaced a cyclic table reference with an arbitrary string value instead of just throwing the request altogether, or cutting that index and throwing a warning…

Expected behavior:

Datastores should be strict, explicit, and verbose in the fact that they don’t support storing these tables.

An error should be thrown specifying that the table cannot be stored in Datastores, as it contains mixed (sub-)tables / arrays with holes / arrays not starting at 0 / cyclic references, and the table should not be stored altogether.

Additional information:

For tables with holes, there’s some really dodgy behavior going on.

This one saves properly: (hole at [3] instead of [2])
{ [1] =1, [2] = 2, [4] = 3 }

This one gets cut off after index 1: (hole at [2])
{ [1] =1, [3] = 2, [4] = 3 }

8 Likes

This is actually a lot more messed up than I thought and covers way more cases, so I expanded the thread title and the descriptions/repro cases. Particularly the additional notes and cyclic table reference malformation are funky.

Thank you for this report. We are investigating options for solving this. We think that this truncation of mixed tables is common to all roblox lua apis that accept tables, so we are also trying to figure out if/how the solution should/should not apply to other apis.

1 Like