Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/obsidian/src/utils/importNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export const fetchNodeTypeSchemasForInstances = async ({
.select("source_local_id, schema_id")
.eq("space_id", spaceId)
.eq("is_schema", false)
.eq("arity", 0)
.eq("is_relation", false)
.in("source_local_id", nodeInstanceIds);

if (instanceError || !instanceRows) {
Expand All @@ -365,7 +365,7 @@ export const fetchNodeTypeSchemasForInstances = async ({
.select("id, source_local_id, name")
.eq("space_id", spaceId)
.eq("is_schema", true)
.eq("arity", 0)
.eq("is_relation", false)
.in("id", schemaIds);

if (schemaError || !schemaRows) {
Expand Down
4 changes: 2 additions & 2 deletions apps/obsidian/src/utils/importRelations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ const findOrCreateTriple = async ({

/**
* Fetch relation instances from a remote space. Relation instances are concepts with
* is_schema=false and schema_id pointing to a relation type (arity=2).
* is_schema=false and schema_id pointing to a relation type
*/
export const fetchRelationInstancesFromSpace = async ({
client,
Expand All @@ -187,7 +187,7 @@ export const fetchRelationInstancesFromSpace = async ({
)
.eq("space_id", spaceId)
.eq("is_schema", false)
.gt("arity", 0);
.eq("is_relation", true);

if (error || !instances) {
console.warn("Error fetching relation instances:", error);
Expand Down
2 changes: 1 addition & 1 deletion apps/obsidian/src/utils/publishNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export const ensurePublishedRelationsAccuracy = async ({
.select("source_local_id")
.eq("space_id", context.spaceId)
.eq("is_schema", false)
.gt("arity", 0);
.eq("is_relation", true);
if (syncedRelationIdsResult.error) {
console.error(
"Could not get synced relation ids",
Expand Down
10 changes: 5 additions & 5 deletions apps/obsidian/src/utils/syncDgNodesToSupabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ const getLastNodeSchemaSyncTime = async (
.select("last_modified")
.eq("space_id", spaceId)
.eq("is_schema", true)
.eq("arity", 0)
.eq("is_relation", false)
.order("last_modified", { ascending: false })
.limit(1)
.maybeSingle();
Expand All @@ -200,7 +200,7 @@ const getLastRelationSchemaSyncTime = async (
.select("last_modified")
.eq("space_id", spaceId)
.eq("is_schema", true)
.gt("arity", 0)
.eq("is_relation", true)
.order("last_modified", { ascending: false })
.limit(1)
.maybeSingle();
Expand All @@ -216,7 +216,7 @@ const getLastRelationSyncTime = async (
.select("last_modified")
.eq("space_id", spaceId)
.eq("is_schema", false)
.gt("arity", 0)
.eq("is_relation", true)
.order("last_modified", { ascending: false })
.limit(1)
.maybeSingle();
Expand Down Expand Up @@ -350,7 +350,7 @@ const buildChangedNodesFromNodes = async ({
.from("my_concepts")
.select("source_local_id")
.eq("space_id", context.spaceId)
.eq("arity", 0)
.eq("is_relation", false)
.eq("is_schema", false)
.order("id"),
1000,
Expand Down Expand Up @@ -517,7 +517,7 @@ const convertDgToSupabaseConcepts = async ({
.from("my_concepts")
.select("source_local_id,literal_content")
.eq("is_schema", true)
.eq("arity", 0)
.eq("is_relation", false)
.eq("space_id", context.spaceId)
.is("literal_content->>template_content", null);
// could not filter on only absent keys, this includes nulls
Expand Down
2 changes: 1 addition & 1 deletion apps/obsidian/src/utils/templateImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const fetchTemplateImportCandidates = async ({
"id, source_local_id, name, literal_content, author_id, space_id, last_modified",
)
.eq("is_schema", true)
.eq("arity", 0)
.eq("is_relation", false)
.eq("name", trimmedNodeTypeName)
.neq("space_id", context.spaceId);

Expand Down
2 changes: 1 addition & 1 deletion apps/roam/src/utils/cleanupOrphanedNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const getAllNodeSchemasFromSupabase = async (
.select("source_local_id")
.eq("space_id", spaceId)
.eq("is_schema", true)
.eq("arity", 0)
.eq("is_relation", false)
.not("source_local_id", "is", null);

if (error) {
Expand Down
4 changes: 2 additions & 2 deletions apps/roam/src/utils/syncDgNodesToSupabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,7 @@ const getAllMissingOrNewDiscourseNodes = async ({
.from("my_concepts")
.select("source_local_id")
.eq("space_id", spaceId)
.eq("arity", 0)
.eq("is_relation", false)
.eq("is_schema", false)
.order("id"),
1000,
Expand Down Expand Up @@ -938,7 +938,7 @@ const getSharedNodeInstanceSourceLocalIds = async ({
.select("source_local_id")
.eq("space_id", spaceId)
.eq("is_schema", false)
.eq("arity", 0)
.eq("is_relation", false)
.order("source_local_id"),
1000,
);
Expand Down
90 changes: 57 additions & 33 deletions packages/database/doc/concept_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Note: Implementors can skip this section.

This generalization is rooted in prior experience with RDF, TopicMaps, semantic Frames, and David Spivak's [algebraic database](https://arxiv.org/abs/1602.03501) representation. In all cases, any type is defined by the attributes it can have. RDF calls them properties, Minsky's frames uses the term slots, etc. RDF/OWL distinguishes literal attributes (`owl: DatatypeProperty`, stored in the `literal_content` column) from attributes which are references to other objects (`owl:ObjectProperty` stored in the `reference_content` column). We adopted the term roles for ObjectProperties from TopicMaps. Spivak's work gives this distinction a categorical grounding.

Unifying relations and nodes in particular allows relations to both have extra attributes (as in a PropertyGraph vs. a strict RDF graph) and be referred to (either as the source/destination of another Relation, or as a target of an `ObjectProperty`.) Cliff Joslyn described this recursive mathematical structure as a [übergraph](https://arxiv.org/abs/1704.05547v1). It can also be expressed as reified relations in RDF-\*.
Unifying relations and nodes in particular allows non-intrinsic relations to both have extra attributes (as in a PropertyGraph vs. a strict RDF graph) and be referred to (either as the source/destination of another Relation, or as a target of an `ObjectProperty`.) Cliff Joslyn described this recursive mathematical structure as a [übergraph](https://arxiv.org/abs/1704.05547v1). It can also be expressed as reified relations in RDF-\*.

### Use cases

Expand All @@ -49,6 +49,7 @@ Base:

CrossAppNodeSchema:
# Base and...
slotDefinitions: Record<string, LocalId> # Forthcoming
metadata?: Json
label: string
template?: string
Expand Down Expand Up @@ -80,6 +81,7 @@ CrossAppNode:
nodeType: LocalId
content.direct: InlineCrossAppContent
content.full?: InlineCrossAppTypedContent
slots: Record<string, LocalId> # Forthcoming

CrossAppRelation:
# Base and...
Expand All @@ -102,40 +104,59 @@ CrossAppRelation:
| `authorId` | <-> | `author_local_id` => `author_id` |
| `metadata` | <- | `literal_content` |

Note on metadata: It is read from the database, but currently not written.
Note on metadata: It is read from the database, but currently not written. We will phase it out.

When some specific keys of `literal_content` are mapped to a CrossApp field, those key-value pairs are not included again in the `metadata`. Thus `metadata` is a grab-bag for residual data. (Eg color for now.)

Residual (not otherwise accounted for) keys in Obsidian frontmatter are mapped to `literal_content->source_data`. (Not through CrossApp.)

#### Matching of CrossAppNodeSchema and Concept

| CrossAppNodeSchema | Concept | value |
| ------------------ | ----------------------------------- | ----- |
| `label` | `name` | {} |
| `template` | `literal_content->template_content` | |
| `templateTitle` | `literal_content->template` | |
| - | `is_schema` | true |
| - | `schema_id` | null |
| - | `arity` | 0 |
| - | `reference_content` | {} |

Note that this does not yet allow for ObjectProperties to be defined; this would require to define `literal_content->roles`, corresponding `reference_content`s, and change the `arity`. See below.
| CrossAppNodeSchema | Concept | value |
| ---------------------- | ----------------------------------- | ----- |
| `label` | `name` | {} |
| `template` | `literal_content->template_content` | |
| `templateTitle` | `literal_content->template` | |
| - | `is_schema` | true |
| - | `schema_id` | null |
| - | `arity` | 0 |
| - | `is_relation` | false |
| `slotDefinition` | `reference_content` | |
| `keys(slotDefinition)` | `literal_content->roles` | |

In the case of a node with ObjectRelations, such as Evidence, we would see something like:

| CrossAppNode | Concept | value |
| ---------------------- | ------------------------ | ------------------------------------- |
| - | `arity` | 1 |
| `slotDefinitions` | `reference_content` | {"sourceDocument": <id of Reference>} |
| `keys(slotDefinition)` | `literal_content->roles` | {"roles":["sourceDocument"]} |

Also note: In Obsidian, where we do not go through CrossAppNodeSchema, the label is also assigned to `literal_content->label`.

Query filter: `.eq("arity",0).eq("is_schema", true)`
Query filter: `.eq("is_relation", false).eq("is_schema", true)`

#### Matching of CrossAppNode and Concept

| CrossAppNode | Concept | value |
| ----------------------- | ----------------------------------------------- | ------------------ |
| `content->direct->text` | `name` | |
| - | `is_schema` | false |
| - | `arity` | 0 |
| - | `reference_content` | {} |
| - | `is_relation` | false |
| `nodeType` | `schema_represented_by_local_id` => `schema_id` | ref to Node schema |
| - | `arity` | 0 |
| `slots` | `reference_content` | {} |

In the case of a node with ObjectRelations, such as Evidence, we would see:

| CrossAppNode | Concept | value |
| ------------ | ------------------- | ------------------------ |
| - | `arity` | 1 |
| `slots` | `reference_content` | {"sourceDocument": <id>} |

Query filter: `.eq("arity",0).eq("is_schema", false)`
The keys of the slots (instance variables) should match those defined in the `literal_content->roles` of the corresponding schema, as shown above.

Query filter: `.eq("is_relation", false).eq("is_schema", false)`

#### Matching of CrossAppRelationTypeSchema and Concept (Obsidian only)

Expand All @@ -148,8 +169,9 @@ Query filter: `.eq("arity",0).eq("is_schema", false)`
| - | `reference_content` | {} |
| `label` | `name`, `literal_content->label` | |
| `complement` | `literal_content->complement` | |
| - | `is_relation` | true |

Query filter: `.eq("arity",2).eq("is_schema", true).is("reference_content->source", "null")`
Query filter: `.eq("is_relation", true).eq("is_schema", true).is("reference_content->source", "null")`

#### Matching of Obsidian CrossAppRelationTripleSchema and Concept

Expand All @@ -165,10 +187,13 @@ Query filter: `.eq("arity",2).eq("is_schema", true).is("reference_content->sourc
| `sourceType` | `reference_content->source` | ref to Node schema |
| `destinationType` | `reference_content->destination` | ref to Node schema |
| `relation` | `reference_content->relation_type` | ref to RelationType schema |
| - | `is_relation` | true |

Note that the relationType in `reference_content->relation_type` is not here playing the role of a slot definition, as it will not be defined in relation instances; so it does not belong in the roles. It can be thought of as an instance variable of the `CrossAppRelationTripleSchema` class itself.

Note that putting the relationType in `reference_content->relation_type` without a corresponding role was a hackish shortcut, and should be revisited (see below.)
Theory: If we were materialize five metaclasses for the five kinds of objects, we would define the `relation_type` as a role (`slotDefinition`) of the `CrossAppRelationTripleSchema` metaclass, and its range constraint would have to point for a metaclass corresponding to `RelationTypeSchema`. It may help conceptually, but there is no reason to materialize those metaclasses in the database.

Query filter: `.eq("arity",2).eq("is_schema", true).not("reference_content->relation_type", "is", "null")`
Query filter: `.eq("is_relation", true).eq("is_schema", true).not("reference_content->relation_type", "is", "null")`

#### Matching of Roam CrossAppRelationTripleSchema and Concept

Expand All @@ -183,12 +208,13 @@ Query filter: `.eq("arity",2).eq("is_schema", true).not("reference_content->rela
| `complement` | `literal_content->complement` | |
| `sourceType` | `reference_content->source` | ref to Node schema |
| `destinationType` | `reference_content->destination` | ref to Node schema |
| - | `is_relation` | true |

Query filter: `.eq("arity",2).eq("is_schema", true).not("reference_content->source", "is", "null").is("reference_content->relation_type", "null")`
Query filter: `.eq("is_relation", true).eq("is_schema", true).not("reference_content->source", "is", "null").is("reference_content->relation_type", "null")`

In most cases, you would want both Roam and Obsidian RelationTripleSchemas, hence you would simply use:

Combined query filter: `.eq("arity",2).eq("is_schema", true).not("reference_content->source", "is", "null")`
Combined query filter: `.eq("is_relation", true).eq("is_schema", true)`

#### Matching of Obsidian CrossAppRelation and Concept

Expand All @@ -201,22 +227,20 @@ Combined query filter: `.eq("arity",2).eq("is_schema", true).not("reference_cont
| `relationType` | `schema_id` (in Roam) | ref to RelationTriple schema |
| `source` | `reference_content->source` | ref to Node |
| `destination` | `reference_content->destination` | ref to Node |
| - | `is_relation` | true |

Query filter: `.eq("arity",2).eq("is_schema", false)`
Again, the keys of the slots match those defined in the `literal_content->roles` of the corresponding schema. In the case of relations, those must include `source` and `destination`. Those slot definitions are implicit. We may allow extra slot definitions for relationTypeSchema some day, but those would be besides the implicit `source` and `destination`.

### Design considerations and future changes
Query filter: `.eq("is_relation", true).eq("is_schema", false)`

#### Arity and ObjectProperties

The arity on either a schema or instance is based on the size of the `literal_content->roles` array in the schema.
The `reference_content` allows either single or multiple values (`Record<string, number|number[]>`). The `reference_content` values are collated in a computed column `refs`, whose index allows for efficient filters on sql queries before digging into the `reference_content` jsonb.
### Design considerations and future changes

As a first approximation, we distinguished relations from nodes using `arity==2`, but this precludes using ObjectProperties.
#### Relations, arity and ObjectProperties

To remedy this, we propose adding a computed column `is_relation`, which would check whether (source, destination) are both in the schema's roles. The query filters using `arity` would be redefined to use `is_relation`.
The arity on either a schema or instance is based on the size of the `literal_content->roles` array in the schema. Think of the `roles` literal as a class variable that defines which variables will exist in instances.

Internal node references such as the Evidence's Source can then be expressed using roles and internal relations.
The `reference_content` allows either single or multiple values (`Record<string, number|number[]>`). The `reference_content` values are collated in a computed column `refs`, whose index allows for efficient filters on sql queries before digging into the `reference_content` jsonb. Currently, the slots only accommodate single values.

In `RelationTripleSchema`, we refer to the `RelationTypeSchema` with a `relation_type` entry in the `reference_content` column. This, unusually, is not backed by an entry in the `roles`. This is a deviation from the mental model, but in the current situation, adding that role would break the `arity=2` checks. Introducing the `is_relation` column will also allow this to be part of the roles.
Relations are concepts (schemas or instances) whose schemas have the special roles `source` and `destination`. This is captured in a computed column `is_relation`.

Note that we probably won't add a range constraint in that case; the constraint should require the `relation_type` to be any `RelationTypeSchema`, but there is no row materializing this meta-class.
Internal node references such as the Evidence's Source can be expressed using a slotDefinition which will show up in the schema's roles and slots which will show up in the instances' `reference_content`.
2 changes: 2 additions & 0 deletions packages/database/schema.puml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class "Concept" [[{An abstract concept, claim or relation}]] {
{field} reference_content : JSON
{field} literal_content : JSON
{field} is_schema : boolean
{field} is_relation : boolean
}
"LocalAccess" --> "0..1" "Space" : "space"
"Content" --> "0..1" "Space" : "space"
Expand Down Expand Up @@ -104,6 +105,7 @@ class "ConceptSchema" [[{A Concept that describes a schema (type) for other conc
{field} reference_content(i) : JSON
{field} literal_content(i) : JSON
{field} is_schema(i) : boolean
{field} is_relation(i) : boolean
}
"Concept" --> "1" "ConceptSchema" : "schema"
"Concept" ^-- "ConceptSchema"
Expand Down
2 changes: 1 addition & 1 deletion packages/database/schema.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions packages/database/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ classes:
range: integer
required: true
ifabsent: "0"
description: The number of roles in this relation; nodes have zero, binary relations have 2, etc.
description: The number of roles in this relation
schema:
range: ConceptSchema
required: true
Expand All @@ -345,12 +345,17 @@ classes:
literal_content:
range: JSON
required: true
description: "Aspects of the concept that have literal values. `{[key: string]: any}`"
description: "Aspects of the concept that have literal values. `{[key: string]: any}`. For schemas, may include a list of `roles` for instances."
ifabsent: "{}"
is_schema:
range: boolean
required: true
ifabsent: "false"
is_relation:
range: boolean
required: true
description: "Whether the concept acts as a relation, i.e. has `source` and `destination` among its roles or its schema's roles"
ifabsent: "false"
represented_by:
description: This concept is explicitly represented by a given content unit
range: Content
Expand Down
Loading