Search code examples
erlangmnesia

Altering Mnesia Tables/Record Types


I am not entirely understanding how records work with Mnesia, and what is the effect of changing record types. Here is in example:

Erlang/OTP 17 [erts-6.2] [source] [64-bit] [async-threads:10] [kernel-poll:false]
Eshell V6.2  (abort with ^G)

1> c(test).
{ok,test}
2> mnesia:start().
ok
3> test:reset_db().
{atomic,ok}
4> test:add_sensor("1a0",12,erlang:now()).
{atomic,ok}
5> test:add_sensor("1a1",10,erlang:now()).
{atomic,ok}
6> test:list_sensors().
[{sensors,"1a0",12,{1484,392274,122051}},
 {sensors,"1a1",10,{1484,392280,673175}}]
7> test:list_sensors_id().
["1a0","1a1"]

So this all makes sense - we created a table (test:reset_db) with records of type "sensors", and added two sensors (test:add_sensor). We see both records of "sensors" type. Now lets modify the table:

8> test:update_db().
{atomic,ok}
9> test:list_sensors().
[{sensors_new,"1a0",12,{1484,392274,122051},0},
 {sensors_new,"1a1",10,{1484,392280,673175},0}]
10> test:list_sensors_id().
["1a0","1a1"]
11> q().
ok

So this is the part I don't get - we updated our table (test:update_db) so now we have "sensors" tables with "sensors_new" record types - this is consistent with what we see on test:list_sensors, but what I don't get is why test:list_sensors_id() still works? Is there a mapping between #sensors_new.name and #sensors.id? How Erlang/Mnesia knows that "X#sensors.id" and "X#sensors_new.name" are the same field after translation? Or what am I missing?

-include_lib("stdlib/include/qlc.hrl").
-module(test).
-compile(export_all).

-record(sensors,{id,val,update}).
-record(sensors_new,{name,val,update,type}).

reset_db() ->
  mnesia:delete_table(sensors),
  mnesia:create_table(sensors, [{attributes, record_info(fields, sensors)}]).

update_db() ->
  Transformer = fun(X) when is_record(X, sensors) ->
  #sensors_new{name = X#sensors.id,
              val = X#sensors.val,
              update = X#sensors.update,
              type = 0} end,
  {atomic, ok} = mnesia:transform_table(sensors,Transformer,record_info(fields, sensors_new),sensors_new).


add_sensor(Id, Val, Update) ->
  Row = #sensors{id=Id, val=Val, update=Update},
  F = fun() ->
    mnesia:write(Row)
    end,
  mnesia:transaction(F).

list_sensors() ->
  do(qlc:q([X || X <-mnesia:table(sensors)])).

list_sensors_id() ->
  do(qlc:q([X#sensors.id || X <-mnesia:table(sensors)])).

do(Q) ->
    F = fun() -> qlc:e(Q) end,
    {atomic, Val} = mnesia:transaction(F),
    Val.

Solution

  • Quick review first: Records in Erlang are just syntactic sugar over tuples with the record name added as the first element. When you access the Nth field in a record R, Erlang is actually converting that to use element(N+1, R). In this example, #sensors.id and #sensors_new.name both give the same value of 2, so accessing either of those is just element(2, R).

    Now by default, accessing the fields of a record through the wrong type should fail (barring something like two records having the same name). If a variable Foo is bound to a record of the type #sensors, but you try to access something through #sensors_new, like say Foo#sensors_new.name, that will normally throw a badrecord error, like this:

    4> Foo = #sensors{id="bar"}.
    #sensors{id = "bar"}
    5> Foo#sensors_new.name.
    ** exception error: {badrecord,sensors_new}
    

    However, it seems that check is bypassed when using QLCs (the QLC parse transform passes in no_strict_record_tests, disabling that "strict" record field access test). If I had to guess, I'd say this was an optimization combined with an assumption that you wouldn't use the same QLC if you swapped the backing record.