Skip to content

Commit

Permalink
all: Require timeseries id to be Int8 and autogenerate them
Browse files Browse the repository at this point in the history
  • Loading branch information
lutter committed Feb 14, 2024
1 parent 8ba5018 commit a2a0243
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 12 deletions.
12 changes: 9 additions & 3 deletions docs/aggregations.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ populate the `Stats` aggregations whenever a given hour or day ends.

The type for the raw data points is defined with an `@entity(timeseries:
true)` annotation. Timeseries types are immutable, and must have an `id`
field and a `timestamp` field. The `timestamp` is set automatically by
`graph-node` to the timestamp of the current block; if mappings set this
field, it is silently overridden when the entity is saved.
field and a `timestamp` field. The `id` must be of type `Int8` and is set
automatically so that ids are increasing in insertion order. The `timestamp`
is set automatically by `graph-node` to the timestamp of the current block;
if mappings set this field, it is silently overridden when the entity is
saved.

Aggregations are declared with an `@aggregation` annotation instead of an
`@entity` annotation. They must have an `id` field and a `timestamp` field.
Expand Down Expand Up @@ -142,6 +144,10 @@ The following aggregation functions are currently supported:
| `first` | First value |
| `last` | Last value |

The `first` and `last` aggregation function calculate the first and last
value in an interval by sorting the data by `id`; `graph-node` enforces
correctness here by automatically setting the `id` for timeseries entities.

## Querying

_This section is not implemented yet, and will require a bit more thought
Expand Down
9 changes: 7 additions & 2 deletions graph/src/schema/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2208,13 +2208,13 @@ type Gravatar @entity {
fn aggregation() {
const SCHEMA: &str = r#"
type Data @entity(timeseries: true) {
id: Bytes!
id: Int8!
timestamp: Int8!
value: BigDecimal!
}
type Stats @aggregation(source: "Data", intervals: ["hour", "day"]) {
id: Bytes!
id: Int8!
timestamp: Int8!
sum: BigDecimal! @aggregate(fn: "sum", arg: "value")
}
Expand Down Expand Up @@ -2249,6 +2249,11 @@ type Gravatar @entity {
"_change_block",
"and",
"id",
"id_gt",
"id_gte",
"id_in",
"id_lt",
"id_lte",
"or",
"timestamp",
"timestamp_gt",
Expand Down
24 changes: 21 additions & 3 deletions graph/src/schema/input_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1951,13 +1951,30 @@ mod validations {
})
}

/// The `@entity` direactive accepts two flags `immutable` and
/// The `@entity` directive accepts two flags `immutable` and
/// `timeseries`, and when `timeseries` is `true`, `immutable` can
/// not be `false`.
///
/// For timeseries, also check that there is a `timestamp` field of
/// type `Int8`
/// type `Int8` and that the `id` field has type `Int8`
fn validate_entity_directives(&self) -> Vec<SchemaValidationError> {
fn id_type_is_int8(object_type: &s::ObjectType) -> Option<SchemaValidationError> {
let field = match object_type.field(&*ID) {
Some(field) => field,
None => {
return Some(Err::IdFieldMissing(object_type.name.to_owned()));
}
};

match field.field_type.value_type() {
Ok(ValueType::Int8) => None,
Ok(_) | Err(_) => Some(Err::IllegalIdType(format!(
"Timeseries `{}` must have an `id` field of type `Int8`",
object_type.name
))),
}
}

fn bool_arg(
dir: &s::Directive,
name: &str,
Expand Down Expand Up @@ -1990,7 +2007,8 @@ mod validations {
object_type.name.clone(),
))
} else {
Self::valid_timestamp_field(object_type)
id_type_is_int8(object_type)
.or_else(|| Self::valid_timestamp_field(object_type))
}
} else {
None
Expand Down
10 changes: 7 additions & 3 deletions runtime/test/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1605,13 +1605,13 @@ async fn test_store_ts() {

let schema = r#"
type Data @entity(timeseries: true) {
id: Bytes!
id: Int8!
timestamp: Int8!
amount: BigDecimal!
}
type Stats @aggregation(intervals: ["hour"], source: "Data") {
id: Bytes!
id: Int8!
timestamp: Int8!
max: BigDecimal! @aggregate(fn: "max", arg:"amount")
}"#;
Expand All @@ -1635,8 +1635,12 @@ async fn test_store_ts() {
)
.expect("Setting 'Data' is allowed");

// This is very backhanded: we generate an id the same way that
// `store_setv` should have.
let did = IdType::Int8.generate_id(12, 0).unwrap();

// Set overrides the user-supplied timestamp for timeseries
let data = host.store_get(DATA, DID).unwrap().unwrap();
let data = host.store_get(DATA, &did.to_string()).unwrap().unwrap();
assert_eq!(Some(&Value::from(block_time)), data.get("timestamp"));

let err = host
Expand Down
7 changes: 6 additions & 1 deletion runtime/wasm/src/host_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,12 @@ impl<C: Blockchain> HostExports<C> {

Self::expect_object_type(&entity_type, "set")?;

let entity_id = if entity_id == "auto" {
let entity_id = if entity_id == "auto"
|| entity_type
.object_type()
.map(|ot| ot.timeseries)
.unwrap_or(false)
{
if self.data_source_causality_region != CausalityRegion::ONCHAIN {
return Err(anyhow!(
"Autogenerated IDs are only supported for onchain data sources"
Expand Down

0 comments on commit a2a0243

Please sign in to comment.