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:
- Open a baseplate and publish it to a game with Studio API access.
- 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
- 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 }