From 7c3d479b72f446528c91d7a646de5135a54bf393 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Tue, 16 Dec 2025 16:55:22 +0000 Subject: [PATCH 01/24] [metadata-1] doc started --- sql-research/2025-12-16-metadata.md | 316 ++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 sql-research/2025-12-16-metadata.md diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md new file mode 100644 index 0000000..69cd926 --- /dev/null +++ b/sql-research/2025-12-16-metadata.md @@ -0,0 +1,316 @@ +# Metadata + +## Purpose and Scope of this Document + +## Goals + +### Long Term + + - Interoperability! + - Introspection + - Ingestion and Export + - Schema on write and read + - Validation and Parsing + - Transformation and Conversion + - Repair + - Generate format for data + - Generate data from format + - Data Modelling + +### Short Term + + - Manually written + - Data Modelling + - Validation and Parsing + - Transformation and Conversion + - Ingestion + + +## Elements of Metadata + +### Schemas + +A schema is a set of **integrity constraints** imposed on sets of data +that together form a queryable unit of data. +In our terminology, such units of data are called **datasets**. + +Integrity Constraints are formulas (recipes) that describe **structured data**. +They describe how data can be read from and written to **data stores** and +how they can be effectivley queried. + +A query retrieves data, either in isolation or - more typically and more relevant - +in combination according to their integrity constraints. How data can be meaninfully combined +is defined by **relations**. + +Some relations are predefined in the schema, others are defined by quries. +Predefined relations are called **tables**. +Tables are the building blocks of data modelling. +They carry additional metadata, in particular **statistics**. + +Relations are organised in terms of **columns and rows**. +Currently we don't exploit columns for fast retrieval or storage -- +we rely on storage engines to provide that, +but we might, in the future, add statistics to columns or row groups +to accelerate ingestion and, in particular, retrieval. + +Integrity Constraints are + +- Types, which restrict the domain of data assigned to a column; + +- Additional Column constraints (e.g. nullability); + +- Tables; + +- Keys. + +**Keys** define internal relations between rows in a table. +(Strictly speaking, they define relations between columns in on row. But don't let us be pedantic.) +Keys are + +- Primary Keys (PK), which uniquely define one row in a table. + A table has at most one PK; + +- Secondary Keys (SK), which define a set of rows in a table. + A table may have a no, one or many SKs. + (NOTE: this is **not** a standard term in relational algebra); + +- Foreign Keys (FK), which define a secondary key in a table + that corresponds to the primary key in another table; + +**Indices** are search structures defined over tables. +They are associated with keys. + +Today, tables and indices are the only predefined relations. +In the future other types of relations may be considered, such as: + +- Views + +- Materialised Views + +- Triggers + +- Stored Procedures + +The **lifecycles** of schemas and the data they describe are independent. +The same schema may be applied to many datasets. +As such the data the principial metadata of a dataset is its schema. +Today we call the schema of a dataset its **kind**. +From the perspective of generic datasets, this term is unfortunate. +The natural term is schema. + +### Types + +Types restrict domains of data that can be associated with a column. +Unlike types in programming languages, they don't imply how they should be outlined in memory. +That depends on how the specific language (or tool) interprets metadata. + +The **type system** proposed here is therefore **much simpler** than those found in modern programming languages. +Particularly, the type system does not define a type hierarchy and very little type semantics. +We can rely on languages and tools to provide such advanced facilities. + +However, types indicate how data may be used. +The **type name** shall support an intuitive understanding of the type's semantic. + +Types are distinguished in **primitive types** and **complex types**. + +Primitive types are defined in terms of + +- Type name (mandatory) + +- Size + +- Unit + +- Range + +- Encoding. + +A `uint8` can be defined as: + +```json +{ + "name": "integer", + "size": 8, + "range": "0..255", +} +``` + +The unit of size is defined per type name. Here it is bits. + +A boolean: + +```json +{ + "name": "bits", + "size": 1, +} +``` + +A more involved example: + +```json +{ + "name": "timestamp", + "size": 64, + "unit": second, + "range": "1230940800..", +} +``` + +For convenience, type labels can be defined (and then used in schemas), e.g. +`uint8`, `bool`, `unix_timestamp`, `float32`, `double`. +There should further be a format convention to actually specify types like: +`int(8)[0..255]`, or in this case even: `uint(8)`. + +The role of precision and unit is very similar. In the timestamp example, +we could haven chosen to represent the unit as precision. +However, precision is always a number, while unit is an enum defined on the level of type names. +Note: We can use the SI to predefine a meaningful set of units. +If we want to be crazy, we can implement the whole thing. + +Base types are: + +- Bit Pattern (`bits`), limit: 64 bit + +- Integer (`int`), limit: 64 bit + +- Floating Point Number (`float`) + +- Timestamp (`timestamp`) + +- Char (`char`), one or more bytes, but always the indicated number of bytes. Encoded in UTF-8 by default. + +- String (`varchar`), one or more bytes, may vary in this range. Encoded in UTF-8 by default. + +- Text (`text`), unlimited number of bytes encoded in UTF-8 by default. + +- Blob (`blob`), unlimited binary object. + +Complex Types are defined in terms of + +- Type name (mandatory) + +- Base type + +- Size + +- Precision. + +Complex types are: + +- Biginteger (`bigint`), integers with more than 64bit, arbitrary length. + It can be restricted explicitly (and then corresponds to an array) or + unrestricted (and then corresponds to a list). + +- Arbitrary Precision Number (`decimal`), arbitrary size and arbitrary precision. + +- Array (`array`): `array(1024)` + +- List (`list`): `list` + +- Enum (`enum`) + +- Union (`union`) + +- Structure (`struct`) + +- Mapping (`map`) + +- JSON (`json`) + +Examples: + +### Keys and Indices + +Today we only have one "PK" - block number - +which results naturally from the predefined structure of blockchain data. +Chunk summaries available in memory enable portals to search for workers quickly. + +This logic can easily be generalised for keys with that increase with time +(and thus with the order ingestion). +Since this is the by far fastest way to find data by key, an indicator +(perhaps a column modifier) should be used for keys that follow this logic. +Auto-increment keys, for instance, behave in the same way. + +With time-incrementing keys, a timestamp search index comes almost for free. +The last timestamp is part of our chunk summary and can therefore be used +to quickly find a range of blocknumber near it. This is a two-level search, +but nevertheless fast. + +For PKs that are not aligned with time and for all other indices, +we need explicit search structures. + +I assume that datasets, although generic, can still be stored in chunks of reasonable size. +Chunks may lose some of their performance advantages they have for our data +(where they are grouped into small hierarchies of related tables (_blocks_ - _transactions_ - etc.) + +Note: +Datasets with many table may need to be broken down into smaller sets of tables, +which would then lead to different kinds of chunks for the same datasets. +This can be modelled in terms of _subdatasets_. +These subsets must be defined by the user. Otherwise, it would be difficult for us +to automate ingestion. + +Generic chunks without time-increasing keys are not nicely aligned like our chunks. +Rather they contain arbitrary collections of keys. Today we may ingest the keys + +`chunk_1: [1 5 9 3]` + +Tomorrow we may have + +`chunk_2: [8 2 7 6]` + +In other words, chunks are still time-related, while keys are not. +The natural partitioning is therefore from key range to ingestion time range. +These shards must be defined by users: +they know their data skew, size and ingestion speed. + +The summary of generic chunks therefore contains the first and last **ingestion** timestamp, +and we find chunks using a map `key range -> time range`. + +The search proceeds in two steps: + +1. For a given set of keys, search the relevant chunk. + +1. Search the chunk in the assignment and proceed with worker requests. + +Note: +With growing numbers of user datasets we cannot keep all relevant data +(maps, assignments, chunks, etc.) in memory in one portal. We will need +to shard datasets over portals. + +We may also want to explore other kind of indices, for example: + +- Bitmap Index + +- Radix Tree + +- B+Tree. + +### Real-Time Data + + +### Statistics + + - Purpose: + Support queries / estimate duration + + - Long term: + Column Stats + + - Short term: + - Row count per table (mandatory, but can be average / estimate) + - PK / SK / Index count + - Cardinality estimation + +## Metadata Format + +### JSON + +### SquidDL + +### IPLD + +## Annex + +### Example JSON Schema From 88837b3c16096c01776f531fe372105989a83134 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Tue, 16 Dec 2025 18:04:04 +0000 Subject: [PATCH 02/24] Fix formatting and clarify metadata definitions Updated metadata documentation with corrections and clarifications. --- sql-research/2025-12-16-metadata.md | 42 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 69cd926..69ae77e 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -89,7 +89,11 @@ In the future other types of relations may be considered, such as: - Triggers -- Stored Procedures +- Stored Procedures. + +Schemas shall also include elements to define real-time data. +This may be an endpoint from which data are read and a stored procedure +that transforms data and passes then on to an internal storage facility. The **lifecycles** of schemas and the data they describe are independent. The same schema may be applied to many datasets. @@ -152,14 +156,15 @@ A more involved example: { "name": "timestamp", "size": 64, - "unit": second, + "unit": "second", "range": "1230940800..", } ``` For convenience, type labels can be defined (and then used in schemas), e.g. `uint8`, `bool`, `unix_timestamp`, `float32`, `double`. -There should further be a format convention to actually specify types like: +Fundamental type like bool, double and integer variants should be built in. +Concerning formats, there should a convention to actually specify types like: `int(8)[0..255]`, or in this case even: `uint(8)`. The role of precision and unit is very similar. In the timestamp example, @@ -174,13 +179,13 @@ Base types are: - Integer (`int`), limit: 64 bit -- Floating Point Number (`float`) +- Floating Point Number (`float`), always 64 bit - Timestamp (`timestamp`) - Char (`char`), one or more bytes, but always the indicated number of bytes. Encoded in UTF-8 by default. -- String (`varchar`), one or more bytes, may vary in this range. Encoded in UTF-8 by default. +- String (`varchar`), one or more bytes, may vary within this range. Encoded in UTF-8 by default. - Text (`text`), unlimited number of bytes encoded in UTF-8 by default. @@ -200,25 +205,24 @@ Complex types are: - Biginteger (`bigint`), integers with more than 64bit, arbitrary length. It can be restricted explicitly (and then corresponds to an array) or - unrestricted (and then corresponds to a list). - -- Arbitrary Precision Number (`decimal`), arbitrary size and arbitrary precision. + unrestricted (and then corresponds to a list), e.g.: `bigint` or `bigint(128)`; -- Array (`array`): `array(1024)` +- Arbitrary Precision Number (`decimal`), arbitrary size and arbitrary precision, e.g.: + `decimal(10,2)`, `decimal(2)`; -- List (`list`): `list` +- Array (`array`): `array(1024)`; -- Enum (`enum`) +- List (`list`): `list`; -- Union (`union`) +- Enum (`enum`): `enum`; -- Structure (`struct`) +- Union (`union`): `union`; -- Mapping (`map`) +- Structure (`struct`): `struct`; -- JSON (`json`) +- Mapping (`map`): `map`; -Examples: +- JSON (`json`). ### Keys and Indices @@ -307,10 +311,6 @@ We may also want to explore other kind of indices, for example: ### JSON -### SquidDL +### SquidL ### IPLD - -## Annex - -### Example JSON Schema From d11347e3a9efda25f6234f8f1b562706fad749b9 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 17 Dec 2025 10:49:28 +0000 Subject: [PATCH 03/24] Revise metadata documentation for accuracy and clarity Updated terminology and corrected size units in metadata documentation. Added sections on conversions and statistics, and improved clarity in various explanations. --- sql-research/2025-12-16-metadata.md | 150 ++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 40 deletions(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 69ae77e..e72f564 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -42,7 +42,7 @@ A query retrieves data, either in isolation or - more typically and more relevan in combination according to their integrity constraints. How data can be meaninfully combined is defined by **relations**. -Some relations are predefined in the schema, others are defined by quries. +Some relations are predefined in the schema, others are generated by queries. Predefined relations are called **tables**. Tables are the building blocks of data modelling. They carry additional metadata, in particular **statistics**. @@ -134,12 +134,12 @@ A `uint8` can be defined as: ```json { "name": "integer", - "size": 8, + "size": 1, "range": "0..255", } ``` -The unit of size is defined per type name. Here it is bits. +The unit of size is always byte. A boolean: @@ -155,7 +155,7 @@ A more involved example: ```json { "name": "timestamp", - "size": 64, + "size": 8, "unit": "second", "range": "1230940800..", } @@ -167,19 +167,16 @@ Fundamental type like bool, double and integer variants should be built in. Concerning formats, there should a convention to actually specify types like: `int(8)[0..255]`, or in this case even: `uint(8)`. -The role of precision and unit is very similar. In the timestamp example, -we could haven chosen to represent the unit as precision. -However, precision is always a number, while unit is an enum defined on the level of type names. Note: We can use the SI to predefine a meaningful set of units. If we want to be crazy, we can implement the whole thing. Base types are: -- Bit Pattern (`bits`), limit: 64 bit +- Bit Pattern (`bits`), limit: 8 bytes -- Integer (`int`), limit: 64 bit +- Integer (`int`), limit: 8 bytes -- Floating Point Number (`float`), always 64 bit +- Floating Point Number (`float`), always 8 bytes - Timestamp (`timestamp`) @@ -205,7 +202,7 @@ Complex types are: - Biginteger (`bigint`), integers with more than 64bit, arbitrary length. It can be restricted explicitly (and then corresponds to an array) or - unrestricted (and then corresponds to a list), e.g.: `bigint` or `bigint(128)`; + unrestricted (and then corresponds to a list), e.g.: `bigint` or `bigint(256)`; - Arbitrary Precision Number (`decimal`), arbitrary size and arbitrary precision, e.g.: `decimal(10,2)`, `decimal(2)`; @@ -223,30 +220,60 @@ Complex types are: - Mapping (`map`): `map`; - JSON (`json`). - + +#### Conversions + +Metadata are used by + +- Portals + +- Workers + +- Clients (e.g. DuckDB extension, Squid SDK) + +- AI Agents? + +Type information therefore needs to be converted to different languages / tools: +Rust, C++, Typescript, Python, etc. + +There are two strategies to deal with it: + +1. A library in a language that can easily be integrated into other languages (i.e. ANSI C) + +1. Native implementation for each language. + +Note: The first option would be my choice for Rust, C++ and Python - but not for Typescript. + +In any way it should be clear how to interpret types and the associated data, either by + +- A detailed specification (which is hard for the general case. +If we know the set of languages, we can just indicate the target types and behaviour for each language.) + +- A reference implementation. + ### Keys and Indices -Today we only have one "PK" - block number - +Today we only have one _PK_ - block number - which results naturally from the predefined structure of blockchain data. -Chunk summaries available in memory enable portals to search for workers quickly. +Chunk summaries available in memory enable portals to search for block numbers quickly. -This logic can easily be generalised for keys with that increase with time -(and thus with the order ingestion). +This logic can easily be generalised for keys with that **increase with time** +(and thus with the order of ingestion). Since this is the by far fastest way to find data by key, an indicator (perhaps a column modifier) should be used for keys that follow this logic. Auto-increment keys, for instance, behave in the same way. -With time-incrementing keys, a timestamp search index comes almost for free. -The last timestamp is part of our chunk summary and can therefore be used +With time-incrementing keys, a **timestamp search** index comes almost for free. +The last timestamp in a chunk is part of our **chunk summary** and can therefore be used to quickly find a range of blocknumber near it. This is a two-level search, but nevertheless fast. For PKs that are not aligned with time and for all other indices, -we need explicit search structures. +we need **explicit search structures**. I assume that datasets, although generic, can still be stored in chunks of reasonable size. Chunks may lose some of their performance advantages they have for our data -(where they are grouped into small hierarchies of related tables (_blocks_ - _transactions_ - etc.) +where they are grouped into small hierarchies of related tables (_blocks_ - _transactions_ - etc.) Note: Datasets with many table may need to be broken down into smaller sets of tables, @@ -256,7 +283,7 @@ These subsets must be defined by the user. Otherwise, it would be difficult for to automate ingestion. Generic chunks without time-increasing keys are not nicely aligned like our chunks. -Rather they contain arbitrary collections of keys. Today we may ingest the keys +Rather they contain **arbitrary collections** of keys. Today we may ingest the keys `chunk_1: [1 5 9 3]` @@ -264,26 +291,25 @@ Tomorrow we may have `chunk_2: [8 2 7 6]` -In other words, chunks are still time-related, while keys are not. -The natural partitioning is therefore from key range to ingestion time range. -These shards must be defined by users: -they know their data skew, size and ingestion speed. +In other words, chunks are still time-related, whereas keys are not. +The natural partitioning is therefore from **key range to ingestion time range**. +Key range shards should be defined by users: they know their data size, ingestion speed and data skew. -The summary of generic chunks therefore contains the first and last **ingestion** timestamp, -and we find chunks using a map `key range -> time range`. +The summary of generic chunks therefore shall contain the first and last **ingestion** timestamp; +it is than easy for us to find chunks using a map `key range -> time range`. The search proceeds in two steps: -1. For a given set of keys, search the relevant chunk. +1. For a given set of keys, search the relevant chunks. -1. Search the chunk in the assignment and proceed with worker requests. +1. Search the chunks in the **assignment** and proceed with worker requests. Note: -With growing numbers of user datasets we cannot keep all relevant data +With a growing number of user datasets we cannot keep all relevant data (maps, assignments, chunks, etc.) in memory in one portal. We will need -to shard datasets over portals. +to **shard datasets over portals**. -We may also want to explore other kind of indices, for example: +We may also want to explore other kinds of indices, for example: - Bitmap Index @@ -293,20 +319,64 @@ We may also want to explore other kind of indices, for example: ### Real-Time Data +I assume that the ingestion cycle for real-time data is as follows: + +1. Receive data from a provider though an endpoint + +1. Transform the data + +1. Pass them on to a ingestion facility (e.g. in a local database). + +We thus need: + +- A set of endpoints from where to obtain the input + +- A stored procedure (which is user-defined) to transform the input + +- A library that stores real-time data (which is provided by us). + +Relevant for metadata: + +- Metadata (schemas?) shall contain the endpoint URL (or the set of URLs) + +- Metadata (schemas?) shall provide the concept of **stored procedures** + +- The system (portal?) shall provide a mechanism to invoke stored procedures periodically or event-based. ### Statistics - - Purpose: - Support queries / estimate duration +The purpose of statics is to enable + +- Efficient queries + +- Estimations of duration (for user's convenience) + +Query engines (e.g. DuckDB) use statistics for query planning. +The engine may decide, for example, to download an small table (and perhaps store it persistently) +to perform joins more efficiently. Without estimations about the size of tables that would not be possible. + +For a user working with an interactive client, it is inconvenient not to know how long the query will take (10 seconds? 10 hours?). +Indicating an appromixmated duration (e.g. by means of a progress bar) is extremely helpful in these cases. + +For the time being valuable statistics (averages and estimates) are: + +- Row count per table + +- Row count per key + +- Cardinality estimates. - - Long term: - Column Stats +Unlike schema definitions, statistics change frequently and shall be updated periodically. +Portals shall provide an endpoint - - Short term: - - Row count per table (mandatory, but can be average / estimate) - - PK / SK / Index count - - Cardinality estimation +- from where updates on statistcs can be polled periodically or + +- to which the client can subscribe updates. +In the future, we may want to explore statistics also for other levels. +Especially, column groups are interesting for worker-side optimisations and +client-side optimisations based on materialised views. + ## Metadata Format ### JSON From 60873edebd4665e2761793d127dce903c3410fe9 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 17 Dec 2025 12:16:46 +0000 Subject: [PATCH 04/24] Enhance metadata document with detailed discussions Expanded the metadata document to include detailed discussions on metadata purpose, goals, and formats, along with specific short-term and long-term objectives. --- sql-research/2025-12-16-metadata.md | 118 +++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 19 deletions(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index e72f564..8bfb94e 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -2,29 +2,75 @@ ## Purpose and Scope of this Document +This is a first iteration to start the discussion about metadata for **generic data** modelling and **iteroperability**. +It is not yet a complete specification; it rather highlights relevant topics and suggests possible solutions. +The document discusses in particular + +- Schema definition + +- Type system + +- Keys and Indices + +- Real-time data + +- Statistics + +- Format + ## Goals +The overall goals of defining metadata are to provide + +- interoperability for **generic data** between different components +(portals, workers, SDKs, SQL tool integrations - such as the DuckDB extension) + +- a **modelling facility** to define generic data in the first place. + +We are currently working with known and - to some extent - homogeneous data. +Generic interoperability is much harder. +We will need + +- Unambiguous data definition + +- Expressive power for data modelling. + ### Long Term - - Interoperability! - - Introspection - - Ingestion and Export - - Schema on write and read - - Validation and Parsing - - Transformation and Conversion - - Repair - - Generate format for data - - Generate data from format - - Data Modelling +In the long run, metadata may enable + +- Validation on ingestion and data loading + +- Parsing of data "for free" + +- Automated ingestion + +- Automated data transformation and Conversion + +- Automated data repair + +- Automated Export + +- Data Generation + +- Integration with standard tools + +- Industry-standard modelling facilities. ### Short Term - - Manually written - - Data Modelling - - Validation and Parsing - - Transformation and Conversion - - Ingestion - +In the short run, particularly to implement the +[Proof-of-Product](https://github.com/subsquid/specs/blob/main/sql-research/2025-11-27-agents-pop-1.md) +the goals are more moderate: + +- Provide data modelling by means of hand-written metadata + +- Interoperability rooted in such hand-written documents + +- Hand-crafted ingestion pipelines + +- Validation and Parsing of data for different components +(portals, workers, SDKs, the DuckDB extension). ## Elements of Metadata @@ -139,7 +185,7 @@ A `uint8` can be defined as: } ``` -The unit of size is always byte. +The unit of size is always byte. (Or is it more convenient to define integers and bit patterns in bit?) A boolean: @@ -229,7 +275,7 @@ Metadata are used by - Workers -- Clients (e.g. DuckDB extension, Squid SDK) +- Clients (e.g. DuckDB extension, SDKs) - AI Agents? @@ -297,6 +343,9 @@ Key range shards should be defined by users: they know their data size, ingestio The summary of generic chunks therefore shall contain the first and last **ingestion** timestamp; it is than easy for us to find chunks using a map `key range -> time range`. +Key range shards shall be part the schema definition. +The map, which also contains frequently changing data, shall be part of metadata, +most likely outside the schema definition (similar to statistics). The search proceeds in two steps: @@ -381,6 +430,37 @@ client-side optimisations based on materialised views. ### JSON +Currently metadata are hand-written in JSON and only contain information +relevant for the DuckDB extension. Here is an [example](https://github.com/subsquid/sql4sqd-prototype/blob/main/charts/sql-central/config/ethereum_holesky_1.json). + +JSON is a cumbersome format and lacks facilities for managing interrelated documents with different scopes and lifecycles +(e.g. long-living schema information and frequently updated statistics). +Obvious alternatives are + +- SQL, in particular DDL + +- [IPLD](https://ipld.io/). + ### SquidL -### IPLD +I propose a SQL-based format for schema definitions. +DDL statements in SQL have the format CREATE|DELETE|ALTER TABLE ...`. +For our schema definitions the operation is not needed - it may be convenient for modelling (and then standard SQL can be used). +For metadata I propose a simplified SQL syntax of the form: + +```SQL +TYPE block_number as uint64; +TYPE unix_timestamp as timestamp(8){unit=second}; +TYPE hash_as_hex as char(64); +TABLE blocks ( + number block_number PRIMARY KEY, + hash hash_as_hex, + parent_hash hash_as_hex, + timestamp unix_timestamp +); +``` + +Here is an example for a solana-based dataset. + +###IPLD +TBD From f840ff735fc747253916a8a2b74473673beaa697 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 17 Dec 2025 13:16:19 +0000 Subject: [PATCH 05/24] Refine language and structure in metadata document Revised the metadata document to improve clarity and consistency in language, including updates to the purpose, schema definitions, and type system descriptions. --- sql-research/2025-12-16-metadata.md | 162 +++++++++++++++------------- 1 file changed, 86 insertions(+), 76 deletions(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 8bfb94e..6b95547 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -2,9 +2,11 @@ ## Purpose and Scope of this Document -This is a first iteration to start the discussion about metadata for **generic data** modelling and **iteroperability**. -It is not yet a complete specification; it rather highlights relevant topics and suggests possible solutions. -The document discusses in particular +This document represents a first iteration intended to initiate discussion on metadata +for **generic data modeling** and **interoperability**. +It does not constitute a complete specification; +instead, it identifies key topics and proposes possible approaches. +In particular, the document discusses: - Schema definition @@ -28,7 +30,7 @@ The overall goals of defining metadata are to provide - a **modelling facility** to define generic data in the first place. We are currently working with known and - to some extent - homogeneous data. -Generic interoperability is much harder. +Generic interoperability is significantly harder. We will need - Unambiguous data definition @@ -61,7 +63,7 @@ In the long run, metadata may enable In the short run, particularly to implement the [Proof-of-Product](https://github.com/subsquid/specs/blob/main/sql-research/2025-11-27-agents-pop-1.md) -the goals are more moderate: +the goals are less ambitious: - Provide data modelling by means of hand-written metadata @@ -76,27 +78,27 @@ the goals are more moderate: ### Schemas -A schema is a set of **integrity constraints** imposed on sets of data -that together form a queryable unit of data. +A schema is a set of **integrity constraints** imposed on a set of data +that together forms a queryable unit of data. In our terminology, such units of data are called **datasets**. Integrity Constraints are formulas (recipes) that describe **structured data**. They describe how data can be read from and written to **data stores** and -how they can be effectivley queried. +how they can be queried efficiently. -A query retrieves data, either in isolation or - more typically and more relevant - -in combination according to their integrity constraints. How data can be meaninfully combined +A query retrieves data, either in isolation or - more typically and more relevantly - +in combination according to its integrity constraints. How data can be meaningfully combined is defined by **relations**. -Some relations are predefined in the schema, others are generated by queries. +Some relations are predefined in the schema, others are produced by queries. Predefined relations are called **tables**. Tables are the building blocks of data modelling. They carry additional metadata, in particular **statistics**. -Relations are organised in terms of **columns and rows**. -Currently we don't exploit columns for fast retrieval or storage -- -we rely on storage engines to provide that, -but we might, in the future, add statistics to columns or row groups +Relations are organised into **rows and columns**. +At present we don't exploit column-level metadata for retrieval or storage optimisation -- +we rely on storage engines to provide these capabilities. +In the future, we may add statistics to columns or row groups to accelerate ingestion and, in particular, retrieval. Integrity Constraints are @@ -109,25 +111,26 @@ Integrity Constraints are - Keys. -**Keys** define internal relations between rows in a table. -(Strictly speaking, they define relations between columns in on row. But don't let us be pedantic.) +**Keys** define internal relations between rows in a table; +They constrain column values across rows. Keys are -- Primary Keys (PK), which uniquely define one row in a table. +- Primary Keys (PK) uniquely define one row in a table. A table has at most one PK; -- Secondary Keys (SK), which define a set of rows in a table. - A table may have a no, one or many SKs. - (NOTE: this is **not** a standard term in relational algebra); +- Secondary keys (SKs) are one or more columns whose values may repeat + and are used to identify and retrieve sets of rows. + A table may have zero, one, or many SKs. + (Note: “secondary key” is not standard relational-algebra terminology.) -- Foreign Keys (FK), which define a secondary key in a table +- Foreign Keys (FK) define a secondary key in one table that corresponds to the primary key in another table; **Indices** are search structures defined over tables. They are associated with keys. Today, tables and indices are the only predefined relations. -In the future other types of relations may be considered, such as: +In the future, other types of relations may be considered, such as: - Views @@ -137,31 +140,33 @@ In the future other types of relations may be considered, such as: - Stored Procedures. -Schemas shall also include elements to define real-time data. -This may be an endpoint from which data are read and a stored procedure -that transforms data and passes then on to an internal storage facility. +Schemas shall also include elements for defining real-time data. +This may include an endpoint from which data is read, +and a stored procedure (or equivalent processing step) +that transforms data and passes it on to an internal API. The **lifecycles** of schemas and the data they describe are independent. -The same schema may be applied to many datasets. -As such the data the principial metadata of a dataset is its schema. -Today we call the schema of a dataset its **kind**. +The same schema may be applied to multiple datasets. +Therefore, the principial metadata of a dataset is a reference to a schema. + +Today we refer to a dataset's schema as its **kind**. From the perspective of generic datasets, this term is unfortunate. The natural term is schema. ### Types -Types restrict domains of data that can be associated with a column. +Types restrict the domain of data that may be associated with a column. Unlike types in programming languages, they don't imply how they should be outlined in memory. -That depends on how the specific language (or tool) interprets metadata. +The physical in-memory layout depends on how the specific language (or tool) interprets metadata. -The **type system** proposed here is therefore **much simpler** than those found in modern programming languages. -Particularly, the type system does not define a type hierarchy and very little type semantics. -We can rely on languages and tools to provide such advanced facilities. +The proposed **type system** is therefore **much simpler** than those found in modern programming languages. +In particular, it does not define a type hierarchy or generics and provides only minimal type semantics. +More advanced facilites can be delegated to surrounding languages and tools. However, types indicate how data may be used. -The **type name** shall support an intuitive understanding of the type's semantic. +Therefore, the **type name** shall support an intuitive understanding of the type's semantic. -Types are distinguished in **primitive types** and **complex types**. +Types are distinguished into **primitive types** and **complex types**. Primitive types are defined in terms of @@ -207,7 +212,7 @@ A more involved example: } ``` -For convenience, type labels can be defined (and then used in schemas), e.g. +For convenience, the system may support **type labels**, e.g. `uint8`, `bool`, `unix_timestamp`, `float32`, `double`. Fundamental type like bool, double and integer variants should be built in. Concerning formats, there should a convention to actually specify types like: @@ -282,7 +287,7 @@ Metadata are used by Type information therefore needs to be converted to different languages / tools: Rust, C++, Typescript, Python, etc. -There are two strategies to deal with it: +Strategies to deal with it include 1. A library in a language that can easily be integrated into other languages (i.e. ANSI C) @@ -299,31 +304,30 @@ If we know the set of languages, we can just indicate the target types and behav ### Keys and Indices -Today we only have one _PK_ - block number - -which results naturally from the predefined structure of blockchain data. -Chunk summaries available in memory enable portals to search for block numbers quickly. +Today, we have only one _PK_ - block number - +which follows naturally from the predefined structure of blockchain data. +Chunk summaries held in memory enable portals to locate the relevant block ranges quickly. -This logic can easily be generalised for keys with that **increase with time** -(and thus with the order of ingestion). -Since this is the by far fastest way to find data by key, an indicator -(perhaps a column modifier) should be used for keys that follow this logic. -Auto-increment keys, for instance, behave in the same way. +This logic can easily be generalised to keys that **increase over time** +(and therefore with ingestion order). +Since this is by far the fastest way to locate data by key, schemas should +provide an indicator (e.g. a column modifier) for keys with this property. +Auto-increment keys, for instance, behave in this way. -With time-incrementing keys, a **timestamp search** index comes almost for free. +With time-incrementing keys, a **timestamp search** index can be obtained almost for free. The last timestamp in a chunk is part of our **chunk summary** and can therefore be used -to quickly find a range of blocknumber near it. This is a two-level search, -but nevertheless fast. +to quickly locate a key range nearby. This yields a two-level lookup, but it is still fast in practice. -For PKs that are not aligned with time and for all other indices, +For PKs that are not aligned with time - and for all other indices - we need **explicit search structures**. I assume that datasets, although generic, can still be stored in chunks of reasonable size. -Chunks may lose some of their performance advantages they have for our data -where they are grouped into small hierarchies of related tables (_blocks_ - _transactions_ - etc.) +However, chunks may lose some of their performance advantages they have for our data +where they are organised into small hierarchies of related tables (_blocks_ - _transactions_ - etc.) Note: Datasets with many table may need to be broken down into smaller sets of tables, -which would then lead to different kinds of chunks for the same datasets. +which would result in different kinds of chunks for the same datasets. This can be modelled in terms of _subdatasets_. These subsets must be defined by the user. Otherwise, it would be difficult for us to automate ingestion. @@ -337,15 +341,15 @@ Tomorrow we may have `chunk_2: [8 2 7 6]` -In other words, chunks are still time-related, whereas keys are not. -The natural partitioning is therefore from **key range to ingestion time range**. -Key range shards should be defined by users: they know their data size, ingestion speed and data skew. +In other words, chunks remain tied to ingestion time, whereas keys may not. +The natural partitioning therefore maps **key ranges to ingestion time ranges**. +Key-range shards should be defined by users: they know their data size, ingestion speed and data skew. -The summary of generic chunks therefore shall contain the first and last **ingestion** timestamp; -it is than easy for us to find chunks using a map `key range -> time range`. +The generic chunk summary shall contain the first and last **ingestion timestamp**; +it is than easy for us to locate chunks using a map `key range -> time range`. Key range shards shall be part the schema definition. The map, which also contains frequently changing data, shall be part of metadata, -most likely outside the schema definition (similar to statistics). +most likely outside the schema definition itself (similar to statistics). The search proceeds in two steps: @@ -354,9 +358,9 @@ The search proceeds in two steps: 1. Search the chunks in the **assignment** and proceed with worker requests. Note: -With a growing number of user datasets we cannot keep all relevant data -(maps, assignments, chunks, etc.) in memory in one portal. We will need -to **shard datasets over portals**. +As the number of user datasets grows, we cannot keep all relevant data +(maps, assignments, chunks, etc.) in memory in a single portal. We will need +to **shard datasets across portals**. We may also want to explore other kinds of indices, for example: @@ -400,14 +404,14 @@ The purpose of statics is to enable - Estimations of duration (for user's convenience) -Query engines (e.g. DuckDB) use statistics for query planning. -The engine may decide, for example, to download an small table (and perhaps store it persistently) -to perform joins more efficiently. Without estimations about the size of tables that would not be possible. +Query engines (e.g. DuckDB) use statistics for **query planning**. +The engine may decide, for example, to download a small table (and possibly cache it persistently) +to execute joins more efficiently. Without estimates of table size that would not be possible. For a user working with an interactive client, it is inconvenient not to know how long the query will take (10 seconds? 10 hours?). -Indicating an appromixmated duration (e.g. by means of a progress bar) is extremely helpful in these cases. +Indicating an approximate duration (e.g. a progress bar) is extremely helpful. -For the time being valuable statistics (averages and estimates) are: +At present, available statistics (averages and estimates) are: - Row count per table @@ -415,7 +419,7 @@ For the time being valuable statistics (averages and estimates) are: - Cardinality estimates. -Unlike schema definitions, statistics change frequently and shall be updated periodically. +Unlike schema definitions, statistics **change frequently** and shall be refreshed periodically. Portals shall provide an endpoint - from where updates on statistcs can be polled periodically or @@ -423,19 +427,20 @@ Portals shall provide an endpoint - to which the client can subscribe updates. In the future, we may want to explore statistics also for other levels. -Especially, column groups are interesting for worker-side optimisations and +In particular, **column groups** are of interest for worker-side optimisations and client-side optimisations based on materialised views. ## Metadata Format ### JSON -Currently metadata are hand-written in JSON and only contain information -relevant for the DuckDB extension. Here is an [example](https://github.com/subsquid/sql4sqd-prototype/blob/main/charts/sql-central/config/ethereum_holesky_1.json). +At present, metadata is hand-written in JSON and contains only the information +required by the DuckDB extension. +Here is an [example](https://github.com/subsquid/sql4sqd-prototype/blob/main/charts/sql-central/config/ethereum_holesky_1.json). JSON is a cumbersome format and lacks facilities for managing interrelated documents with different scopes and lifecycles -(e.g. long-living schema information and frequently updated statistics). -Obvious alternatives are +(e.g. long-lived schema information and frequently updated statistics). +Obvious alternatives include - SQL, in particular DDL @@ -444,8 +449,13 @@ Obvious alternatives are ### SquidL I propose a SQL-based format for schema definitions. -DDL statements in SQL have the format CREATE|DELETE|ALTER TABLE ...`. -For our schema definitions the operation is not needed - it may be convenient for modelling (and then standard SQL can be used). +DDL statements in SQL typically take the form +`CREATE|DROP|ALTER table_name...`. +For our purposes, interoperability between our components (portas, workers, clients) +the operation itself may not be needed; +however, supporting it for actual data definition could be convenient for modeling +and would allow us to reuse standard SQL tooling. + For metadata I propose a simplified SQL syntax of the form: ```SQL @@ -453,7 +463,7 @@ TYPE block_number as uint64; TYPE unix_timestamp as timestamp(8){unit=second}; TYPE hash_as_hex as char(64); TABLE blocks ( - number block_number PRIMARY KEY, + number block_number PRIMARY KEY, -- missing: indicator for ingestion time alignment hash hash_as_hex, parent_hash hash_as_hex, timestamp unix_timestamp From 30c01c4c605b42ac20a783ef144fc6e65c1dbe6b Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 17 Dec 2025 13:19:16 +0000 Subject: [PATCH 06/24] Fix spelling of 'modeling' in metadata document Corrected spelling of 'modelling' to 'modeling' throughout the document. --- sql-research/2025-12-16-metadata.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 6b95547..c951343 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -27,7 +27,7 @@ The overall goals of defining metadata are to provide - interoperability for **generic data** between different components (portals, workers, SDKs, SQL tool integrations - such as the DuckDB extension) -- a **modelling facility** to define generic data in the first place. +- a **modeling facility** to define generic data in the first place. We are currently working with known and - to some extent - homogeneous data. Generic interoperability is significantly harder. @@ -35,7 +35,7 @@ We will need - Unambiguous data definition -- Expressive power for data modelling. +- Expressive power for data modeling. ### Long Term @@ -57,7 +57,7 @@ In the long run, metadata may enable - Integration with standard tools -- Industry-standard modelling facilities. +- Industry-standard modeling facilities. ### Short Term @@ -65,7 +65,7 @@ In the short run, particularly to implement the [Proof-of-Product](https://github.com/subsquid/specs/blob/main/sql-research/2025-11-27-agents-pop-1.md) the goals are less ambitious: -- Provide data modelling by means of hand-written metadata +- Provide data modeling by means of hand-written metadata - Interoperability rooted in such hand-written documents @@ -92,7 +92,7 @@ is defined by **relations**. Some relations are predefined in the schema, others are produced by queries. Predefined relations are called **tables**. -Tables are the building blocks of data modelling. +Tables are the building blocks of data modeling. They carry additional metadata, in particular **statistics**. Relations are organised into **rows and columns**. @@ -472,5 +472,5 @@ TABLE blocks ( Here is an example for a solana-based dataset. -###IPLD +### IPLD TBD From 01128f33de62f2bf583fd5e5194aa4303a085b99 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 17 Dec 2025 17:01:45 +0000 Subject: [PATCH 07/24] solana.sql example added --- sql-research/2025-12-16-metadata.md | 3 +- sql-research/attachments/solana.sql | 60 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 sql-research/attachments/solana.sql diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index c951343..db9f7f5 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -470,7 +470,8 @@ TABLE blocks ( ); ``` -Here is an example for a solana-based dataset. +[Here](attachments/solana.sql) +is an example for a solana-based dataset. ### IPLD TBD diff --git a/sql-research/attachments/solana.sql b/sql-research/attachments/solana.sql new file mode 100644 index 0000000..2dabec4 --- /dev/null +++ b/sql-research/attachments/solana.sql @@ -0,0 +1,60 @@ +---------------------------------------------------------------------------------------------------- +-- TABLE BLOCK +---------------------------------------------------------------------------------------------------- +TYPE block_number AS UINT64; +TYPE unix_timestamp AS TIMESTAMP(8){unit=second}; +TYPE hash_as_hex AS CHAR(64); + +TABLE blocks ( + number block_number PRIMARY KEY, --#key-time-aligned + hash hash_as_hex, + parent_number block_number, + parent_hash hash_as_hex, + height UINT64; + timestamp unix_timestamp +); + +---------------------------------------------------------------------------------------------------- +-- TABLE TRANSACTIONS +---------------------------------------------------------------------------------------------------- +TYPE tx_index AS UINT32; +TYPE tx_version AS UINT16; +TYPE key_list AS LIST(VARCHAR); + +TYPE address_st AS STRUCT( + account_key LIST(VARCHAR), + readonly_indexes LIST(UINT8), + writeable_indexes LIST(UINT8) +); + +TYPE address_lookup_list AS LIST(address_st); +TYPE signature_list AS LIST(VARCHAR); + +TYPE loaded_addresses_st AS STRUCT( + readonly LIST(VARCHAR), + writable LIST(VARCHAR) +); + +TABLE transactions ( + block_number block_number PRIMARY KEY, --#key-time-aligned + transaction_index tx_index PRIMARY KEY, + version tx_version, + account_keys key_list, + address_table_lookups address_lookup_list, + num_readonly_signed_accounts UINT8, + num_readonly_unsigned_accounts UINT8, + num_required_signatures UINT8, + recent_blockhash VARCHAR, + signatures signature_list, + err VARCHAR, + compute_units_consumed UINT64, + fee UINT64, + loaded_addresses loaded_addresses_st, + has_dropped_log_messages BOOLEAN, + fee_payer VARCHAR, + account_keys_size UINT64, + address_table_lookups_size UINT64, + signatures_size UINT64, + loaded_addresses_size UINT64, + accounts_bloom BLOB +); From 82250f2ea3353b33a90400019647ae5e508b49db Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Thu, 18 Dec 2025 09:00:33 +0000 Subject: [PATCH 08/24] Clarify hash type options in solana.sql Added comment to clarify hash type options. --- sql-research/attachments/solana.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql-research/attachments/solana.sql b/sql-research/attachments/solana.sql index 2dabec4..c94b90b 100644 --- a/sql-research/attachments/solana.sql +++ b/sql-research/attachments/solana.sql @@ -3,7 +3,7 @@ ---------------------------------------------------------------------------------------------------- TYPE block_number AS UINT64; TYPE unix_timestamp AS TIMESTAMP(8){unit=second}; -TYPE hash_as_hex AS CHAR(64); +TYPE hash_as_hex AS CHAR(64); -- or: hash_as_base64 as VARCHAR(44) TABLE blocks ( number block_number PRIMARY KEY, --#key-time-aligned From dbe4b8bdcf108d50a78a3b8a07989543ec2a8814 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Thu, 18 Dec 2025 15:37:21 +0000 Subject: [PATCH 09/24] Revise metadata for clarity and consistency Updated metadata document to improve clarity and consistency in terminology, including changes to key definitions and type representations. --- sql-research/2025-12-16-metadata.md | 50 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index db9f7f5..9b21d5d 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -96,7 +96,7 @@ Tables are the building blocks of data modeling. They carry additional metadata, in particular **statistics**. Relations are organised into **rows and columns**. -At present we don't exploit column-level metadata for retrieval or storage optimisation -- +At present we don't exploit column-level metadata for retrieval or storage optimisation - we rely on storage engines to provide these capabilities. In the future, we may add statistics to columns or row groups to accelerate ingestion and, in particular, retrieval. @@ -107,30 +107,32 @@ Integrity Constraints are - Additional Column constraints (e.g. nullability); -- Tables; - -- Keys. +- Keys (including **referential integrity constraints**) **Keys** define internal relations between rows in a table; They constrain column values across rows. Keys are -- Primary Keys (PK) uniquely define one row in a table. +- **Primary Keys** (PK) uniquely define one row in a table. A table has at most one PK; -- Secondary keys (SKs) are one or more columns whose values may repeat +- **Secondary keys** (SKs) are one or more columns whose values may repeat and are used to identify and retrieve sets of rows. A table may have zero, one, or many SKs. (Note: “secondary key” is not standard relational-algebra terminology.) -- Foreign Keys (FK) define a secondary key in one table +- **Foreign Keys** (FK) define a secondary key in one table that corresponds to the primary key in another table; +Note: multi-column key are possible and should not cause to much overhead. +However, they have impact on memory requirements. + **Indices** are search structures defined over tables. They are associated with keys. +(Indices are not part of relational algebra. They a tool to implement it efficiently.) -Today, tables and indices are the only predefined relations. -In the future, other types of relations may be considered, such as: +Today, tables together with indices defined over them are the only predefined relations. +In the future, other types of relations and other objects may be considered, such as: - Views @@ -205,18 +207,15 @@ A more involved example: ```json { - "name": "timestamp", + "name": "blockchain_timestamp", "size": 8, "unit": "second", "range": "1230940800..", } ``` -For convenience, the system may support **type labels**, e.g. -`uint8`, `bool`, `unix_timestamp`, `float32`, `double`. -Fundamental type like bool, double and integer variants should be built in. -Concerning formats, there should a convention to actually specify types like: -`int(8)[0..255]`, or in this case even: `uint(8)`. +Fundamental types like `bool, `double` and integer variants should be built in. +For convenience, the system may support **type labels** to define common types. Note: We can use the SI to predefine a meaningful set of units. If we want to be crazy, we can implement the whole thing. @@ -258,17 +257,17 @@ Complex types are: - Arbitrary Precision Number (`decimal`), arbitrary size and arbitrary precision, e.g.: `decimal(10,2)`, `decimal(2)`; -- Array (`array`): `array(1024)`; +- Array (`array`): `array(uint64, 1024)`; -- List (`list`): `list`; +- List (`list`): `list(uint64)`; -- Enum (`enum`): `enum`; +- Enum (`enum`): `enum(A|B|C|D)`; -- Union (`union`): `union`; +- Union (`union`): `union(A(typeOfA), B(typeOfB), C(typeOfC))`; -- Structure (`struct`): `struct`; +- Structure (`struct`): `struct(a: typeOfA, b: typeOfB)`; -- Mapping (`map`): `map`; +- Mapping (`map`): `map(typOfKey, typeOfValue)`; - JSON (`json`). @@ -326,11 +325,10 @@ However, chunks may lose some of their performance advantages they have for our where they are organised into small hierarchies of related tables (_blocks_ - _transactions_ - etc.) Note: -Datasets with many table may need to be broken down into smaller sets of tables, -which would result in different kinds of chunks for the same datasets. +Datasets with many tables may need to be broken down into smaller sets of tables, +which would result in different kinds of chunks for the same dataset. This can be modelled in terms of _subdatasets_. -These subsets must be defined by the user. Otherwise, it would be difficult for us -to automate ingestion. +These subsets must be defined by the user. Generic chunks without time-increasing keys are not nicely aligned like our chunks. Rather they contain **arbitrary collections** of keys. Today we may ingest the keys @@ -347,7 +345,7 @@ Key-range shards should be defined by users: they know their data size, ingestio The generic chunk summary shall contain the first and last **ingestion timestamp**; it is than easy for us to locate chunks using a map `key range -> time range`. -Key range shards shall be part the schema definition. +Key range shards shall be part of the schema definition. The map, which also contains frequently changing data, shall be part of metadata, most likely outside the schema definition itself (similar to statistics). From 51222f0979afdac0137322f1067f172a434cc814 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Thu, 18 Dec 2025 15:38:48 +0000 Subject: [PATCH 10/24] Update primary key comments in SQL tables --- sql-research/attachments/solana.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sql-research/attachments/solana.sql b/sql-research/attachments/solana.sql index c94b90b..057ebc9 100644 --- a/sql-research/attachments/solana.sql +++ b/sql-research/attachments/solana.sql @@ -6,7 +6,7 @@ TYPE unix_timestamp AS TIMESTAMP(8){unit=second}; TYPE hash_as_hex AS CHAR(64); -- or: hash_as_base64 as VARCHAR(44) TABLE blocks ( - number block_number PRIMARY KEY, --#key-time-aligned + number block_number PRIMARY KEY, --#timeseries hash hash_as_hex, parent_number block_number, parent_hash hash_as_hex, @@ -36,7 +36,7 @@ TYPE loaded_addresses_st AS STRUCT( ); TABLE transactions ( - block_number block_number PRIMARY KEY, --#key-time-aligned + block_number block_number PRIMARY KEY, --#timeseries transaction_index tx_index PRIMARY KEY, version tx_version, account_keys key_list, From aae50ef1fcebfafae651c86391aa9bf307a0c6cc Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 21 Jan 2026 09:41:28 +0000 Subject: [PATCH 11/24] Fix typo in ingestion timestamp explanation Corrected a typo in the metadata documentation regarding chunk summary. --- sql-research/2025-12-16-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 9b21d5d..7256ac7 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -344,7 +344,7 @@ The natural partitioning therefore maps **key ranges to ingestion time ranges**. Key-range shards should be defined by users: they know their data size, ingestion speed and data skew. The generic chunk summary shall contain the first and last **ingestion timestamp**; -it is than easy for us to locate chunks using a map `key range -> time range`. +it is then easy for us to locate chunks using a map `key range -> time range`. Key range shards shall be part of the schema definition. The map, which also contains frequently changing data, shall be part of metadata, most likely outside the schema definition itself (similar to statistics). From 5ac91806821aec585a6e24b6bd92d39d09a5b1f8 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 21 Jan 2026 09:50:19 +0000 Subject: [PATCH 12/24] Update metadata with comment on key range sharding Add comment regarding key range sharding and chunk handling. --- sql-research/2025-12-16-metadata.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 7256ac7..9fe72b4 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -349,6 +349,10 @@ Key range shards shall be part of the schema definition. The map, which also contains frequently changing data, shall be part of metadata, most likely outside the schema definition itself (similar to statistics). +Comment: Something is missing here - there may be many chunks for any given key range. +Either keys and chunks are sharded already during ingestion time (keeping one chunk per shard open) +or we need a search structure that itself is distributed over workers. + The search proceeds in two steps: 1. For a given set of keys, search the relevant chunks. From ea4a04ccc8c8250eae1223076ea89a3b402ffeab Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Tue, 3 Feb 2026 11:39:17 +0000 Subject: [PATCH 13/24] Enhance metadata document with overview and data types Added an overview section and updated the discussion on data types. --- sql-research/2025-12-16-metadata.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 9fe72b4..0b33e6d 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -8,13 +8,15 @@ It does not constitute a complete specification; instead, it identifies key topics and proposes possible approaches. In particular, the document discusses: +- Overall Structure + - Schema definition - Type system - Keys and Indices -- Real-time data +- Archive and Real-time data - Statistics @@ -76,6 +78,16 @@ the goals are less ambitious: ## Elements of Metadata +### Overview + +The structure of metadata as a whole may be: + +

+ Solution Draft +

+ ### Schemas A schema is a set of **integrity constraints** imposed on a set of data From addbd40a23247ac6710ea2af14598a64f36a3c5d Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Tue, 3 Feb 2026 11:41:02 +0000 Subject: [PATCH 14/24] [metadata-1] overall structure added --- sql-research/attachments/metadata-structure.png | Bin 0 -> 87644 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 sql-research/attachments/metadata-structure.png diff --git a/sql-research/attachments/metadata-structure.png b/sql-research/attachments/metadata-structure.png new file mode 100644 index 0000000000000000000000000000000000000000..80fb85aeb4f5fa81bee8bbfbaad42423b9e6ab6b GIT binary patch literal 87644 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%0DIZYR!#UJkmBrOtOU9jwP?R%rkS+CC5 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 Date: Tue, 3 Feb 2026 13:19:49 +0000 Subject: [PATCH 15/24] Revise metadata structure and propose SQL for schemas Updated metadata structure and elements sections, clarified indexing and routing strategies, and proposed SQL for schema management. --- sql-research/2025-12-16-metadata.md | 86 ++++++++++------------------- 1 file changed, 30 insertions(+), 56 deletions(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 0b33e6d..8b7cba0 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -78,7 +78,9 @@ the goals are less ambitious: ## Elements of Metadata -### Overview +### Assumptions + +### Elements and their Lifecycles The structure of metadata as a whole may be: @@ -88,6 +90,10 @@ The structure of metadata as a whole may be: width="65%"/>

+#### Short-term versus Long-term + +#### Self-certification + ### Schemas A schema is a set of **integrity constraints** imposed on a set of data @@ -141,7 +147,7 @@ However, they have impact on memory requirements. **Indices** are search structures defined over tables. They are associated with keys. -(Indices are not part of relational algebra. They a tool to implement it efficiently.) +(Indices are not part of relational algebra. They are tools to implement it efficiently.) Today, tables together with indices defined over them are the only predefined relations. In the future, other types of relations and other objects may be considered, such as: @@ -293,14 +299,14 @@ Metadata are used by - Clients (e.g. DuckDB extension, SDKs) -- AI Agents? +- AI Agents Type information therefore needs to be converted to different languages / tools: Rust, C++, Typescript, Python, etc. Strategies to deal with it include -1. A library in a language that can easily be integrated into other languages (i.e. ANSI C) +1. A library in a language that can easily be integrated into other languages (i.e. ANSI C or Rust) 1. Native implementation for each language. @@ -352,24 +358,16 @@ Tomorrow we may have `chunk_2: [8 2 7 6]` In other words, chunks remain tied to ingestion time, whereas keys may not. -The natural partitioning therefore maps **key ranges to ingestion time ranges**. -Key-range shards should be defined by users: they know their data size, ingestion speed and data skew. - -The generic chunk summary shall contain the first and last **ingestion timestamp**; -it is then easy for us to locate chunks using a map `key range -> time range`. -Key range shards shall be part of the schema definition. -The map, which also contains frequently changing data, shall be part of metadata, -most likely outside the schema definition itself (similar to statistics). -Comment: Something is missing here - there may be many chunks for any given key range. -Either keys and chunks are sharded already during ingestion time (keeping one chunk per shard open) -or we need a search structure that itself is distributed over workers. +A reasonable approach is a two-level key-to-chunk routing structure. +At the top level, we compute a stable hash of the key and use a fixed-width hash prefix (e.g. 16 bits) +to map the key to a small set of candidate chunks. +This directory is compact, grows slowly, and bounds the expected number of candidate chunks per lookup to a small constant, +independent of the total number of chunks. -The search proceeds in two steps: - -1. For a given set of keys, search the relevant chunks. - -1. Search the chunks in the **assignment** and proceed with worker requests. +At the chunk level, we attach a space-efficient, static membership filter (e.g. a binary fuse, XOR, or ribbon filter) to each chunk. +These filters confirm key membership with low false-positive rates. +Combined with hash-prefix routing, the expected number of unnecessary worker probes can be kept low. Note: As the number of user datasets grows, we cannot keep all relevant data @@ -380,11 +378,15 @@ We may also want to explore other kinds of indices, for example: - Bitmap Index -- Radix Tree +- Zone Index + +- Filters. + +### Archive and Real-Time Data -- B+Tree. +#### Archive -### Real-Time Data +#### Real-Time Data I assume that the ingestion cycle for real-time data is as follows: @@ -452,40 +454,12 @@ At present, metadata is hand-written in JSON and contains only the information required by the DuckDB extension. Here is an [example](https://github.com/subsquid/sql4sqd-prototype/blob/main/charts/sql-central/config/ethereum_holesky_1.json). -JSON is a cumbersome format and lacks facilities for managing interrelated documents with different scopes and lifecycles -(e.g. long-lived schema information and frequently updated statistics). -Obvious alternatives include +### SQL +I propose using SQL, in particular DDL, for schema management containing `CREATE|DROP|ALTER table_name...` statements. +User can then use DuckDB (or other SQL clients) to manage their schema. -- SQL, in particular DDL - -- [IPLD](https://ipld.io/). - -### SquidL - -I propose a SQL-based format for schema definitions. -DDL statements in SQL typically take the form -`CREATE|DROP|ALTER table_name...`. -For our purposes, interoperability between our components (portas, workers, clients) -the operation itself may not be needed; -however, supporting it for actual data definition could be convenient for modeling -and would allow us to reuse standard SQL tooling. - -For metadata I propose a simplified SQL syntax of the form: - -```SQL -TYPE block_number as uint64; -TYPE unix_timestamp as timestamp(8){unit=second}; -TYPE hash_as_hex as char(64); -TABLE blocks ( - number block_number PRIMARY KEY, -- missing: indicator for ingestion time alignment - hash hash_as_hex, - parent_hash hash_as_hex, - timestamp unix_timestamp -); -``` - -[Here](attachments/solana.sql) -is an example for a solana-based dataset. +Note: +Even if we consider schemas largely immutable, a way to correct and adapt schemas is necessary. ### IPLD TBD From b02defd8b6dd7756ccdbfa2a33e4f3da8b3e8513 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Tue, 3 Feb 2026 13:37:34 +0000 Subject: [PATCH 16/24] Update 2025-12-16-metadata.md --- sql-research/2025-12-16-metadata.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 8b7cba0..2875ede 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -22,6 +22,8 @@ In particular, the document discusses: - Format +Out of scope, for the moment are vector databases. + ## Goals The overall goals of defining metadata are to provide @@ -80,6 +82,21 @@ the goals are less ambitious: ### Assumptions +1. Schemas are (largely) immutable. +I say "largely" because the possibility for corrections and adaptions cannot be foreclosed. +The question is then whether such occasional changes should be automated or can be treated as mainenance. + +1. Data are inserted and occasionally deleted, but never updated. + +1. Data are stored in chunks. + +1. Chunks consist of parquet files with related data, each representing one table. + +1. New data (a.k.a. "hotblocks") are handled separately until a new chunk is ready. + +1. Data is always redundant (on s3/r2, on more than one worker, etc.); + no further backups are needed. + ### Elements and their Lifecycles The structure of metadata as a whole may be: @@ -359,7 +376,7 @@ Tomorrow we may have In other words, chunks remain tied to ingestion time, whereas keys may not. -A reasonable approach is a two-level key-to-chunk routing structure. +A reasonable approach is a **two-level key-to-chunk routing** structure. At the top level, we compute a stable hash of the key and use a fixed-width hash prefix (e.g. 16 bits) to map the key to a small set of candidate chunks. This directory is compact, grows slowly, and bounds the expected number of candidate chunks per lookup to a small constant, From 5ad8013ace5c1566ddef76c92c7c450d676fc3e1 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Tue, 3 Feb 2026 16:53:53 +0000 Subject: [PATCH 17/24] Revise metadata document structure and headings Updated section headings and added references to sections for better organization and clarity. --- sql-research/2025-12-16-metadata.md | 160 +++++++++++++++++++++------- 1 file changed, 121 insertions(+), 39 deletions(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 2875ede..3307673 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -1,6 +1,6 @@ # Metadata -## Purpose and Scope of this Document +## 1. Purpose and Scope of this Document This document represents a first iteration intended to initiate discussion on metadata for **generic data modeling** and **interoperability**. @@ -8,23 +8,23 @@ It does not constitute a complete specification; instead, it identifies key topics and proposes possible approaches. In particular, the document discusses: -- Overall Structure +- [Overall Structure](3.1) -- Schema definition +- [Schema definition](3.4) -- Type system +- [Type system](3.5) -- Keys and Indices +- [Keys and Indices](3.6) -- Archive and Real-time data +- [Archive and Real-time data](3.7) -- Statistics +- [Statistics](3.8) -- Format +- [Format](4.) Out of scope, for the moment are vector databases. -## Goals +## 2. Goals The overall goals of defining metadata are to provide @@ -41,7 +41,7 @@ We will need - Expressive power for data modeling. -### Long Term +### 2.1 Long Term In the long run, metadata may enable @@ -63,7 +63,7 @@ In the long run, metadata may enable - Industry-standard modeling facilities. -### Short Term +### 2.2 Short Term In the short run, particularly to implement the [Proof-of-Product](https://github.com/subsquid/specs/blob/main/sql-research/2025-11-27-agents-pop-1.md) @@ -78,26 +78,26 @@ the goals are less ambitious: - Validation and Parsing of data for different components (portals, workers, SDKs, the DuckDB extension). -## Elements of Metadata +## 3. Elements of Metadata -### Assumptions +### 3.1 Assumptions 1. Schemas are (largely) immutable. -I say "largely" because the possibility for corrections and adaptions cannot be foreclosed. +I say "largely" because the possibility for corrections and adaptations cannot be foreclosed. The question is then whether such occasional changes should be automated or can be treated as mainenance. 1. Data are inserted and occasionally deleted, but never updated. 1. Data are stored in chunks. -1. Chunks consist of parquet files with related data, each representing one table. +1. Chunks consist of parquet files with related data; each parquet corresponds to one table (e.g. `blocks`, `transactions`, etc.). -1. New data (a.k.a. "hotblocks") are handled separately until a new chunk is ready. +1. New data (a.k.a. "hotblocks") are handled separately until a new chunk is ready and assigned to workers. -1. Data is always redundant (on s3/r2, on more than one worker, etc.); +1. Data is always redundant (on s3, on more than one worker, etc.); no further backups are needed. -### Elements and their Lifecycles +### 3.2 Elements and their Lifecycles The structure of metadata as a whole may be: @@ -107,11 +107,78 @@ The structure of metadata as a whole may be: width="65%"/>

-#### Short-term versus Long-term +A dataset node contains the metadata describing all one dataset concerning -#### Self-certification +- ownerschip and access +- data location (e.g. s3) +- the schema +- the "archive", i.e. the chunks containing the data (including statistics) +- the "real-time" data (a.k.a. "hotblocks"), i.e. data before they are appended to a chunk. -### Schemas +#### Access and Owner + +This object contains the owner of this dataset (our datasets today would all have the same owner "Squid"). +It also contains access information (public, private, etc.) and elements of access control. +It may contain additional routing information, indicating portals that should serve the dataset in question. +Datasets should be submitted only to the portals for which they are intended. + +This node may change within the life of one dataset: +- when the owner changes (rare) +- when access information changes (more frequent) +- when routing through portals changes (frequent) + +#### Location + +Locations from where data chunks can be downloaded (e.g.: an s3 bucket, a github repository, an ftp server). + +This node may change within the life of one dataset: +- when new locations are added +- when locations are removed or updated + +#### Schema + +See [below](schema). + +This node may change within the life of one dataset with changes to the schema (shall be rare!). + +#### Archive + +Data concerning chunks: +- Statistics at dataset, table, row, and column level. + +This node may change within the life of one dataset: +- Chunks are added (frequent) +- Statistics are recomputed (frequent) +- Chunks are reassigned (frequent) + +#### Real-Time Data + +Data enter the database in "real-time" mode. They are at this point not part of any chunk. +They exist in a database that can be accessed by a portal and served directly from there. + +The real-time node contains information about data sources and ingestion procedures. +The mechanism should generalise today's "hotblocks". For instance: +- A publisher address to which the portal subscribes; +- A stored procedure that is invoked with the data in question; + this procedure may transform the data and should store them in "hotblock" database; +- A connect string for the local database. + +This node may change within the life of one dataset: +- when the publisher or the ingestion process change (occasional). + +### 3.3 Self-Certification + +The whole metadata tree shall be self-certifiable in the sense that + +- Its identifier cryptographically commits to its contents + +- Any tampering is detectable without a trusted authority + +- References between document parts are verifiable. + +For details see below [IPLD](IPLD) + +### 3.4 Schemas A schema is a set of **integrity constraints** imposed on a set of data that together forms a queryable unit of data. @@ -160,7 +227,7 @@ Keys are that corresponds to the primary key in another table; Note: multi-column key are possible and should not cause to much overhead. -However, they have impact on memory requirements. +But they have impact on memory requirements. **Indices** are search structures defined over tables. They are associated with keys. @@ -190,7 +257,22 @@ Today we refer to a dataset's schema as its **kind**. From the perspective of generic datasets, this term is unfortunate. The natural term is schema. -### Types +#### Schema-on-Read or Schema-on-Write + +In the long run, schemas shall be "on-write": data is validated and/or transformed to a fixed schema at ingestion time, +so all stored data conforms to that schema before it is written. +Reaching this goal will take bit of learning. In the short run, schemas will therefore be "on-read": +data is stored with minimal or no upfront structure - it is in particular stored **manually** - +and a schema is applied only when the data is read or queried. + +Non-conforming data is either +- ignored +- converted to NULL (in case of an invalid column) +- flagged as error. +Which strategy is chosen depends on the context. For instance: the DuckDB extension converts unknown or non-conformant fields to NULL; +DuckDB itself may reject data that do not conform to constraints defined in the catalog (e.g. NOT NULL, UNIQUE, etc.). + +### 3.5 Types Types restrict the domain of data that may be associated with a column. Unlike types in programming languages, they don't imply how they should be outlined in memory. @@ -336,7 +418,7 @@ If we know the set of languages, we can just indicate the target types and behav - A reference implementation. -### Keys and Indices +### 3.6 Keys and Indices Today, we have only one _PK_ - block number - which follows naturally from the predefined structure of blockchain data. @@ -399,7 +481,7 @@ We may also want to explore other kinds of indices, for example: - Filters. -### Archive and Real-Time Data +### 3.7 Archive and Real-Time Data #### Archive @@ -429,7 +511,7 @@ Relevant for metadata: - The system (portal?) shall provide a mechanism to invoke stored procedures periodically or event-based. -### Statistics +### 3.8 Statistics The purpose of statics is to enable @@ -444,39 +526,39 @@ to execute joins more efficiently. Without estimates of table size that would no For a user working with an interactive client, it is inconvenient not to know how long the query will take (10 seconds? 10 hours?). Indicating an approximate duration (e.g. a progress bar) is extremely helpful. -At present, available statistics (averages and estimates) are: +Relevant statistics are: -- Row count per table +- Row count per table and chunk -- Row count per key +- Row count per foreign key -- Cardinality estimates. +- Cardinality + +- Min/Max + +- NULL count. Unlike schema definitions, statistics **change frequently** and shall be refreshed periodically. Portals shall provide an endpoint - from where updates on statistcs can be polled periodically or -- to which the client can subscribe updates. - -In the future, we may want to explore statistics also for other levels. -In particular, **column groups** are of interest for worker-side optimisations and -client-side optimisations based on materialised views. +- to which the portal can subscribe updates. -## Metadata Format +## 4. Metadata Format -### JSON +### 4.1 JSON At present, metadata is hand-written in JSON and contains only the information required by the DuckDB extension. Here is an [example](https://github.com/subsquid/sql4sqd-prototype/blob/main/charts/sql-central/config/ethereum_holesky_1.json). -### SQL +### 4.2 SQL I propose using SQL, in particular DDL, for schema management containing `CREATE|DROP|ALTER table_name...` statements. User can then use DuckDB (or other SQL clients) to manage their schema. Note: Even if we consider schemas largely immutable, a way to correct and adapt schemas is necessary. -### IPLD +### 4.3 IPLD TBD From 53d80ce1a6ad46faada1c0102b3ae8c44faa1ef2 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Tue, 3 Feb 2026 18:25:58 +0000 Subject: [PATCH 18/24] Update metadata documentation with scope and details Clarified scope for the first iteration of metadata processing, detailing in-scope and out-of-scope items. Expanded sections on archive and real-time data, including stored procedures and statistics. --- sql-research/2025-12-16-metadata.md | 46 +++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index 3307673..f20207e 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -78,6 +78,22 @@ the goals are less ambitious: - Validation and Parsing of data for different components (portals, workers, SDKs, the DuckDB extension). +For the first iteration we are currently working on, this means: + +OUT OF SCOPE: +- IPLD +- schema-on-write +- real-time data +- generic keys + +IN SCOPE: +- Basic set of statstics (can be computed once, since we have no real-time data) +- Block numbers as keys (done) +- Timestamps as keys (on the way) +- Complex data types +- Consistent error handling +- Better integration in Portal (use Portal datasets, keep datasets + schema in memory) + ## 3. Elements of Metadata ### 3.1 Assumptions @@ -485,6 +501,26 @@ We may also want to explore other kinds of indices, for example: #### Archive +Archive contains all chunk-related data: + +- The assignment file. + Note: this is now implemented separately with flat buffers. + I don't want to imply that it *must* be changed. Conceptually, it belongs to metadata. + But as long as the data are available in portal, there is no need to change it. + +- Chunk metadata (key range if applicable, timestamp range, size, number of keys) + +- Statistics per table, chunk, column + Alternative: + Statistics could be applied directly to the tables in schema. + That would avoid repeating the table information in the archive. + On the other hand, by removing statistics from schema, it becomes slimmer and easier to use. + +- Search structures for generic keys; + Note: the document would most likely only point to a location from where + the structures could be downloaded (e.g. s3); + the portal would then store them locally. + #### Real-Time Data I assume that the ingestion cycle for real-time data is as follows: @@ -505,11 +541,13 @@ We thus need: Relevant for metadata: -- Metadata (schemas?) shall contain the endpoint URL (or the set of URLs) +- The real-time shall contain the endpoint URL (or the set of URLs), + the name of the stored procedure that processes and ingests the data, + and information about the local database that stores the data. -- Metadata (schemas?) shall provide the concept of **stored procedures** +- Schemas shall provide the concept of **stored procedures**. -- The system (portal?) shall provide a mechanism to invoke stored procedures periodically or event-based. +- The portal shall provide a mechanism to invoke stored procedures periodically or event-based. ### 3.8 Statistics @@ -544,6 +582,8 @@ Portals shall provide an endpoint - from where updates on statistcs can be polled periodically or - to which the portal can subscribe updates. + +For the first iteration, statistics will be generated by the ingestion process and stored in plain JSON. ## 4. Metadata Format From c0714bd772e2c0bc04bbbe32878c27804eea1919 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Tue, 3 Feb 2026 18:30:30 +0000 Subject: [PATCH 19/24] Fix link format for Overall Structure section --- sql-research/2025-12-16-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index f20207e..fbc319c 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -8,7 +8,7 @@ It does not constitute a complete specification; instead, it identifies key topics and proposes possible approaches. In particular, the document discusses: -- [Overall Structure](3.1) +- [Overall Structure](#3.1) - [Schema definition](3.4) From 89620a34d81bcc7d7317506ad1b82f8aad4cfcac Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Tue, 3 Feb 2026 18:32:07 +0000 Subject: [PATCH 20/24] Update section title to include 'Assumptions' --- sql-research/2025-12-16-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index fbc319c..a91088f 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -8,7 +8,7 @@ It does not constitute a complete specification; instead, it identifies key topics and proposes possible approaches. In particular, the document discusses: -- [Overall Structure](#3.1) +- [Assumptions and Overall Structure](#3.1-assumptions) - [Schema definition](3.4) From b5b02e78c29c208f2981b1707b9e37590a618486 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 4 Feb 2026 09:48:51 +0000 Subject: [PATCH 21/24] Update link reference in metadata document --- sql-research/2025-12-16-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index a91088f..bc9eea0 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -8,7 +8,7 @@ It does not constitute a complete specification; instead, it identifies key topics and proposes possible approaches. In particular, the document discusses: -- [Assumptions and Overall Structure](#3.1-assumptions) +- [Assumptions and Overall Structure](#3-elements-of-metadata) - [Schema definition](3.4) From 5906500da898058d563e051bf723be0c7be34946 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 4 Feb 2026 09:50:35 +0000 Subject: [PATCH 22/24] Fix link formatting in metadata document --- sql-research/2025-12-16-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index bc9eea0..e37c443 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -10,7 +10,7 @@ In particular, the document discusses: - [Assumptions and Overall Structure](#3-elements-of-metadata) -- [Schema definition](3.4) +- [Schema definition](#3.4) - [Type system](3.5) From ef7df7b745a51d13b15f0ced767451a7d02ad8e2 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 4 Feb 2026 10:18:32 +0000 Subject: [PATCH 23/24] Document IPLD data model and its characteristics Added detailed explanation of IPLD, its structure, and use cases. --- sql-research/2025-12-16-metadata.md | 45 ++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index e37c443..f3da840 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -601,4 +601,47 @@ Note: Even if we consider schemas largely immutable, a way to correct and adapt schemas is necessary. ### 4.3 IPLD -TBD +IPLD (InterPlanetary Linked Data) is a content-addressed data model +for building verifiable, immutable, and linkable data structures, +where each piece of data is identified by a cryptographic hash +and can securely reference other data by hash. +It generalizes Merkle trees into a flexible graph (a Merkle-DAG, where DAG is Directed-Acyclic Graph), +letting systems evolve data by creating new versions while reusing unchanged parts, +enabling integrity-by-construction, efficient deduplication, selective retrieval, +and transport-agnostic distribution across untrusted networks. + +Every IPLD block is identified by a CID, which is: + +``` +CID = hash(codec + multihash(data)) +``` + +where `codec`is a numerical identifier of the format (e.g. `dag-json` or `protobuf`) +and multihash hashes the data, which themselves contain hashes. +If any byte changes, +(1) the hash changes +(2) the CID changes +(3) all parent links break. + +This guarantees strong, built-in integrity verification, but not authenticity. +It is the same core property that makes Git objects, blockchains, and Merkle trees self-verifying — +IPLD merely generalises it across formats and graphs. + +But every change creates a new immutable version; *current* is just a pointer. +This is the rationale for some design decisions, +in particular real-time data should not be part of the IPLD document; +otherwise we would get multiple versions per second. +The document would just point where and how to obtain new data. + +A common way to implement IPLD is through pub/sub: +- New versions (the root CID) are announced on a topic +- subscribers fetch the root and only those parts that (1) they require and (2) changed + (usually not through the topic directly but through some content routing mechanism). + +IPLD is *not* recommended if +- There is no benefit in content addressing, deduplication, or audit history +- When snapshot semantics is too much overhead, that is, + when in-place updates and high-rate writes are required +- SQL-like query/update semantics over the config is required. + +In such cases a conventional content database is far more appropriate. From 7fcfdc1058184bc9f904ae5f07dd63a2c2903b33 Mon Sep 17 00:00:00 2001 From: toschoosqd Date: Wed, 4 Feb 2026 10:43:39 +0000 Subject: [PATCH 24/24] Fix markdown links and formatting in metadata.md Updated markdown links and fixed formatting issues throughout the document. --- sql-research/2025-12-16-metadata.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/sql-research/2025-12-16-metadata.md b/sql-research/2025-12-16-metadata.md index f3da840..8b55b68 100644 --- a/sql-research/2025-12-16-metadata.md +++ b/sql-research/2025-12-16-metadata.md @@ -8,19 +8,21 @@ It does not constitute a complete specification; instead, it identifies key topics and proposes possible approaches. In particular, the document discusses: +- [Overall Goals](#2-goals) + - [Assumptions and Overall Structure](#3-elements-of-metadata) -- [Schema definition](#3.4) +- [Schema definition](#34-schemas) -- [Type system](3.5) +- [Type system](#35-types) -- [Keys and Indices](3.6) +- [Keys and Indices](#36-keys-and-indices) -- [Archive and Real-time data](3.7) +- [Archive and Real-time data](#37-archive-and-real-time-data) -- [Statistics](3.8) +- [Statistics](#38-statistics) -- [Format](4.) +- [Format](#4-metadata-format) Out of scope, for the moment are vector databases. @@ -153,7 +155,7 @@ This node may change within the life of one dataset: #### Schema -See [below](schema). +See [below](#34-schemas). This node may change within the life of one dataset with changes to the schema (shall be rare!). @@ -192,7 +194,7 @@ The whole metadata tree shall be self-certifiable in the sense that - References between document parts are verifiable. -For details see below [IPLD](IPLD) +For details see below [IPLD](#43-ipld) ### 3.4 Schemas @@ -347,7 +349,7 @@ A more involved example: } ``` -Fundamental types like `bool, `double` and integer variants should be built in. +Fundamental types like `bool`, `double`, and `integer` variants should be built in. For convenience, the system may support **type labels** to define common types. Note: We can use the SI to predefine a meaningful set of units. @@ -421,7 +423,7 @@ Rust, C++, Typescript, Python, etc. Strategies to deal with it include -1. A library in a language that can easily be integrated into other languages (i.e. ANSI C or Rust) +1. A library in a language that can easily be integrated into other languages (i.e. ANSI C / Rust) 1. Native implementation for each language. @@ -487,7 +489,7 @@ Combined with hash-prefix routing, the expected number of unnecessary worker pro Note: As the number of user datasets grows, we cannot keep all relevant data (maps, assignments, chunks, etc.) in memory in a single portal. We will need -to **shard datasets across portals**. +file storage or **shard datasets across portals**. We may also want to explore other kinds of indices, for example: