Tables and integrated query
Tables are Adama's primary way to collect and query records. They are similar to tables in relational databases and are based on the same concepts. Adama uses relational database ideas to construct and query tables, which is why the relational database literature is an excellent way to think about data in Adama. Adama tables allow users to store data and query it using a variety of methods, such as sorting, filtering, and grouping. Tables in Adama are powerful tools for managing data and analyzing it in a structured way.
For example, a record can be used to define the structure of a row within a table. Consider this record.
record Rec {
public int id;
public string name;
public int age;
public int score;
}
We can then use this record type to define a table called _records
.
Note, the underscore has become a convention within Adama as tables are never directly visible to users and there is no available privacy modifier.
table<Rec> _records;
A table is a way of organizing information per given record type, and a table is a useful construct that enables many common operations found in data structures, such as adding, updating, and querying records. The above table can contain data such as:
id | name | age | score |
---|---|---|---|
1 | Joe | 45 | 1012 |
2 | Bryan | 49 | 423 |
3 | Jamie | 42 | 892 |
4 | Jordan | 52 | 7231 |
The id
field is a primary key that is automatically generated by the system for each new record.
The name
, age
, and score
fields yield information about a person and their score in the office game of trash can basketball.
Adding rows/records to table via ingestion
The ingestion operator (<-) allows data to be inserted from a variety of sources. For instance, we can simply use ingestion to copy a message into a row.
record Rec {
public int x;
public int y;
}
message Msg {
int x;
int y;
}
table<Rec> tbl;
channel foo(Msg m) {
tbl <- m;
}
#foo {
tbl <- {x:42, y:13};
}
Notes about the special id field. If a record has the 'id' field, then it must be an integer. ingestion will generate an id, and we can get that via the 'as' keyword.
channel foo(Msg m) {
tbl <- m as m_id;
}
#foo {
tbl <- {x:42, y:13} as xy_id;
}
Reactive lists
Lists of records can be filtered, ordered, sequenced, and limited via language integrated query (LINQ).
iterate
First, the iterate
keyword will convert the table<Rec> into a list<Rec>.
public formula all_records =
iterate _records;
Now, by itself, it will list the records in their canonical ordering (by id). It is important to note that the list is lazily constructed up until the time that it is materialized by a consumer, and this enables some query optimizations to happen on the fly.
where
We can suffix a LINQ expression with where to filter items.
public formula young_records =
iterate _records
where age < 18;
where_as
Simillar to where, except we bind the record to a variable rather than build an environment. This has the advantage of eliminating conflicts between the variables feeding the query versus variables within
view int age;
bubble records_younger_than_viewer =
iterate _records
where_as x: x.age < @viewer.age;
indexing!
Yes, we can make things faster by indexing our tables. The index
keyword within a record will indicate how tables should index the record.
public formula lucky_people =
iterate _records where age == 42;
This will accelerate the performance of where
expressions when expressions like age == 42
are detected via analysis.
shuffle
The canonical ordering by id is not great for card games, so we can randomize the order of the list. Now, this will materialize the list.
public formula random_people =
iterate _records shuffle;
public formula random_young_people =
iterate _records where age < 18 shuffle;
reduce
Reduce allows taking the result and reducing it into a map via a function.
public formula grouped_by_x =
iterate _records
reduce on x via (@lambda list: list);
The reduce expression will take the list and group items into lists with a common field value, and then send the list to a function.
For more details, see Maps and reduce
order
Since the canonical ordering by id is the insertion/creation ordering, order allows you to reorder any list.
public formula people_by_age =
iterate _records
order by age asc;
order_dyn
Many times, the user wants to be in control of ordering the results of a query. This is available via order_dyn where the right hand expression is a string containing ordering instructions.
For example, a string of form "age" would sort records by age in ascending order while a string like "-age" would sort in descending order. This compose via a comma such that "age,name" would sort by age first, and everyone with the same age would be sorted by name.
view string sort_people_by;
bubble people =
iterate _records
order_dyn @viewer.sort_people_by;
limit
public formula youngest_person =
iterate _records
order by age asc
limit 1;
offset
With offset, you can skip the first entries with a query.
public formula next_youngest_person =
iterate _records
order by age asc
offset 1
limit 1;
Bulk Assignments
A novel aspect of a reactive list is bulk field assignment, and this allows us to do some nice things. Take the following definition of a Card table representing a deck of cards:
record Card {
public int id;
public int value;
public principal owner;
public int ordering;
}
table<Card> deck;
We can shuffle the deck using shuffle
and bulk assignment.
procedure shuffle() {
int ordering = 0;
(iterate deck shuffle).ordering = ordering++;
}
This assignment of ordering will memorize the results from shuffling. With a single statement, we can deal cards by assigning ownership.
procedure deal_cards(principal who, int count) {
(iterate deck // look at the deck
where owner == @no_one // for each card that isn't own
order by ordering asc // follow the memoized ordering
limit count // deal only $count cards
).owner = who; // for each card, assign an owner to the card
}
This ability makes it simple to update a single field, but it also applies to method invocation as well.
Bulk method execution
Similar to a bulk assignment, bulk method execution allow executing a method on every record within a result.
record Rec {
int x;
method zero() {
x = 0;
}
}
table<Rec> tbl;
procedure zero_records() {
(iterate tbl).zero();
}
Bulk Deletes
Every record has an implicit delete method which will remove the record from the owned table.
procedure trash_cards_randomly(principal who, int count) {
(iterate deck // look at the deck
where owner == who // for each card that isn't own
shuffle // randomize the cards
limit count // deal only $count cards
).delete();
}