translate

package
v0.4.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 23, 2026 License: MIT Imports: 15 Imported by: 0

README

openCypher to PgSQL 16 Translation

Renaming

All variable bindings in an openCypher query are renamed to simplify reordering, decomposition and optimization operations.

match (n) return labels(n)

TODO: match (n)-[r:MemberOf]->()

The above openCypher query has the named variable n renamed to n0 in the below SQL translation:

with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0)
select (s0.n0).kind_ids
from s0;

Scope Management

Translation requires some amount of query planning and result dependency negotiation. This translation driver organizes openCypher query plans as an incrementally named variable in the SQL translation as sN where N is the scope's frame.

Consider the following openCypher query:

match (n), ()-[r]->() return n, r

The above openCypher query, when translated is broken up into two distinct planned elements. First, the plan executes a select for match (n) with n renamed to n0 and saves the projection under the result alias s0 such that s0.n0 contains the result of match (n) return n:

s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0)

The second planned element executes a select and join for the match ()-[r]->() with r renamed to e0. The current implement also eagerly binds all anonymous pattern elements in a given pattern part. The declarations are then saved as a projection under the result alias s1. In addition to joining n1, e0, n2 to the result scope, the previous result s0.n0 is also copied as s1.n0:

s1 as (select s0.n0                                                                     as n0,
               (n1.id, n1.kind_ids, n1.properties)::nodecomposite                        as n1,
               (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0,
               (n2.id, n2.kind_ids, n2.properties)::nodecomposite                        as n2
        from s0,
             edge e0
                 join node n1 on n1.id = e0.start_id
                 join node n2 on n2.id = e0.end_id)

Once the query reaches the final select statement, the following namespace is available authored in openCypher: match (n0), (n1)-[e0]->(n2). The resulting projection may then access them user the result alias s1:

select s1.n0 as n, s1.e0 as r
from s1;

The resulting translation being equal to: return n, r. The translation strives to preserve user preference with regard to projection aliases. In the case of the original openCypher query, the result set the querying client expects is: (n), [r] where n is a node and r is an edge.

The complete translation is:

with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0 from node n0),
     s1 as (select s0.n0                                                                     as n0,
                   (n1.id, n1.kind_ids, n1.properties)::nodecomposite                        as n1,
                   (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0,
                   (n2.id, n2.kind_ids, n2.properties)::nodecomposite                        as n2
            from s0,
                 edge e0
                     join node n1 on n1.id = e0.start_id
                     join node n2 on n2.id = e0.end_id)
select s1.n0 as n, s1.e0 as r
from s1;

Criteria Decomposition

openCypher formally specifies bound elements of a query in the pattern component: match (s)-[r]->(e) return s, r, e and allows referencing of the components in openCpyher expressions: match (s)-[r]->(e) where s.name = '123' return s, r, e.

The translator isolates filtering criteria for bound elements in a data structure named a constraint. Constraints are collected and organized by the bound elements they reference. This allows the translator to reduce, deconstruct and reorder filtering criteria.

For example, consider the following openCypher query:

match (s)-[r]->(e) where s.name = '123' and e:Target and not r.property return s, r, e.

The above openCypher query would first undergo renaming:

match (n0)-[e0]->(n1) where n0.name = '123' and n1:Target and not e0.property return n0 as s, e0 as r, n1 as e.

After renaming the translator will isolate the following constraints:

  • (n0) where n0.name = '123'
  • [e0] where not e0.property
  • (n1) where n1:Target

After renaming the translator, will construct the required SQL AST nodes and assigns constraints to their associated select operations. This is done by comparing the available scope declarations and the requirements of the constraints:

-- match (s)-[r]->(e) where s.name = '123' and e:NodeKind1 and not r.property return s, r, e
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite                        as n0,
                   (e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite as e0,
                   (n1.id, n1.kind_ids, n1.properties)::nodecomposite                        as n1
            from edge e0
                     join node n0 on n0.properties ->> 'name' = '123' and n0.id = e0.start_id
                     join node n1 on n1.kind_ids operator (pg_catalog.&&) array [1]::int2[] and n1.id = e0.end_id
            where not (e0.properties -> 'property')::bool)
select s0.n0 as s, s0.e0 as r, s0.n1 as e
from s0;
Dependency Satisfaction

Query filter expressions may involve operations that can not be isolated to a single bound element. Take for example the following openCypher query: match (s:NodeKind1), (e:NodeKind2) where s.selected or s.tid = e.tid and e.enabled return s, e

The above openCypher query would first undergo renaming:

match (n0:NodeKind1), (n1:NodeKind2) where n0.selected or n0.tid = n1.tid and n1.enabled return n0 as s, n1 as e

After renaming the translator will isolate the following constraints:

  • (n0) where n0:NodeKind1
  • (n1) where n1:NodeKind2
  • (n0), (n1) where n0.selected or n0.tid = n1.tid and n1.enabled

The comparison n0.selected or n0.tid = n1.tid can not be decomposed further and must have both n0 and n1 projected in scope before it can be satisfied. When the query plan first selects for n0 it can only satisfy the constraint (n0) where n0:NodeKind1:

s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0
            from node n0
            where n0.kind_ids operator (pg_catalog.&&) array [1]::int2[])

The second query will project n1 and make it available. This then allows the planner to satisfy both remaining constraints: (n1) where n1:NodeKind2 and (n0), (n1) where n0.selected or n0.tid = n1.tid:

s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1
            from s0,
                 node n1
            where n1.kind_ids operator (pg_catalog.&&) array [2]::int2[] and ((s0.n0).properties -> 'selected')::bool
               or (s0.n0).properties -> 'tid' = n1.properties -> 'tid' and (n1.properties -> 'enabled')::bool)

The complete translation is:

-- match (s:NodeKind1), (e:NodeKind2) where s.selected or s.tid = e.tid and e.enabled return s, e
with s0 as (select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0
            from node n0
            where n0.kind_ids operator (pg_catalog.&&) array [1]::int2[]),
     s1 as (select s0.n0 as n0, (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1
            from s0,
                 node n1
            where n1.kind_ids operator (pg_catalog.&&) array [2]::int2[] and ((s0.n0).properties -> 'selected')::bool
               or (s0.n0).properties -> 'tid' = n1.properties -> 'tid' and (n1.properties -> 'enabled')::bool)
select s1.n0 as s, s1.n1 as e
from s1;

Expansions

  • packages/go/cypher/models/pgsql/translate/expansion.go
  • packages/go/cypher/models/pgsql/translate/translation_cases/pattern_expansion.sql

The translator represents pattern expansion as a recursive CTE to offload as much of the traversal work to the database. Currently, all expansions are hard limited to an expansion depth of 5 steps.

Consider the following openCypher query: match (n)-[*..]->(e) return n, e.

To perform the recursive traversal, the translator first defines a record shape for the CTE to represent the expansion's pathspace:

with s0 as (with recursive ex0(root_id, next_id, depth, satisfied, is_cycle, path)
Column type Usage
root_id Int8 Node that the path originated from. Simplifies referencing the root node of each path.
next_id Int8 Next node to expand to.
depth Int Depth of the current traversal.
satisfied Boolean True if the expansion is satisfied.
is_cycle Boolean True if the expansion is a cycle.
path Int8[] Array of edges in order of traversal.

The translator then formats two queries. First is the primer query that populates the initial pathspace of the expansion:

select e0.start_id,
       e0.end_id,
       1,
       false,
       e0.start_id = e0.end_id,
       array [e0.id]
from edge e0
         join node n0 on n0.id = e0.start_id
         join node n1 on n1.id = e0.end_id

This query is then unioned to a recursive query. Unlike the primer query, the recursive query also includes the temporary table defined by the pathspace record shape ex0:

select ex0.root_id,
       e0.end_id,
       ex0.depth + 1,
       false,
       e0.id = any (ex0.path),
       ex0.path || e0.id
from ex0
         join edge e0 on e0.start_id = ex0.next_id
         join node n1 on n1.id = e0.end_id
where ex0.depth < 5
  and not ex0.is_cycle

The resulting output of the result alias s0 is then populated from the pathspace table:

select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0,
       (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite)
        from edge e0
        where e0.id = any (ex0.path))                     as e0,
       ex0.path                                           as ep0,
       (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1
from ex0
         join edge e0 on e0.id = any (ex0.path)
         join node n0 on n0.id = ex0.root_id
         join node n1 on e0.id = ex0.path[array_length(ex0.path, 1)::int4] and n1.id = e0.end_id

This binds the following additional components to query scope:

Component Type Usage
n0 nodecomposite Root node of each recursively expanded path.
e0 edgecomposite[] edgecomposite array of edges
ep0 int4[] Int4 array of edge identifiers
n1 nodecomposite Terminal node of each recursively expanded path.

The translator then authors the final projection return n, e:

select s0.n0 as n, s0.n1 as e
from s0;

The complete translation is:

-- match (n)-[*..]->(e) return n, e
with s0 as (with recursive ex0(root_id, next_id, depth, satisfied, is_cycle, path) as (select e0.start_id,
                                                                                              e0.end_id,
                                                                                              1,
                                                                                              false,
                                                                                              e0.start_id = e0.end_id,
                                                                                              array [e0.id]
                                                                                       from edge e0
                                                                                                join node n0 on n0.id = e0.start_id
                                                                                                join node n1 on n1.id = e0.end_id
                                                                                       union
                                                                                       select ex0.root_id,
                                                                                              e0.end_id,
                                                                                              ex0.depth + 1,
                                                                                              false,
                                                                                              e0.id = any (ex0.path),
                                                                                              ex0.path || e0.id
                                                                                       from ex0
                                                                                                join edge e0 on e0.start_id = ex0.next_id
                                                                                                join node n1 on n1.id = e0.end_id
                                                                                       where ex0.depth < 5
                                                                                         and not ex0.is_cycle)
            select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0,
                   (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite)
                    from edge e0
                    where e0.id = any (ex0.path))                     as e0,
                   ex0.path                                           as ep0,
                   (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1
            from ex0
                     join edge e0 on e0.id = any (ex0.path)
                     join node n0 on n0.id = ex0.root_id
                     join node n1 on e0.id = ex0.path[array_length(ex0.path, 1)::int4] and n1.id = e0.end_id)
select s0.n0 as n, s0.n1 as e
from s0;
All Shortest Paths
  • packages/go/cypher/models/pgsql/translate/expansion.go
  • packages/go/cypher/models/pgsql/translate/translation_cases/shortest_paths.sql

Recursive harness in PL/pgSQL with non-parameterized primer and recursive queries.

All shortest paths is a search function that will return the first path found and all other paths of the same depth. Naively, this early termination can be authored as a condition in the temporary table of a recursive expansion CTE however recursive CTEs may not perform certain operations on the temporary table allocated for the CTE statement: https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-RECURSIVE.

This means a different construct is required to perform early termination of a shortest-path traversal. For all shortest paths queries, a PL/PgSQL function named asp_harness exists to execute recursive expansion with early termination.

The goal of this effort, even with the caveats of the method pursued, is to prevent the need for unnecessary round trips to the database by offloading as much of the expansion logic to the database as possible. This may result in a sizeable query that is less efficient than its constituent components if reordered, but it allows the implementation to avoid hundreds of thousands or millions of round trips to execute a traversal expansion.

asp_harness
-- All shortest path traversal harness.
create or replace function public.asp_harness(primer_query text, recursive_query text, max_depth int4)
    returns table
            (
                root_id   int4,
                next_id   int4,
                depth     int4,
                satisfied bool,
                is_cycle  bool,
                path      int4[]
            )
as
$$
declare
    depth int4 := 1;
begin
    -- Define two tables to represent pathspace of the recursive expansion. These are temporary and as such are unlogged.
    create temporary table pathspace
    (
        root_id   int4   not null,
        next_id   int4   not null,
        depth     int4   not null,
        satisfied bool   not null,
        is_cycle  bool   not null,
        path      int4[] not null,
        primary key (path)
    ) on commit drop;

    create temporary table next_pathspace
    (
        root_id   int4   not null,
        next_id   int4   not null,
        depth     int4   not null,
        satisfied bool   not null,
        is_cycle  bool   not null,
        path      int4[] not null,
        primary key (path)
    ) on commit drop;

    -- Creating these indexes should speed up certain operations during recursive expansion. Benchmarking should be done
    -- to validate assumptions here.
    create index pathspace_next_id_index on pathspace using btree (next_id);
    create index pathspace_satisfied_index on pathspace using btree (satisfied);
    create index pathspace_is_cycle_index on pathspace using btree (is_cycle);

    create index next_pathspace_next_id_index on next_pathspace using btree (next_id);
    create index next_pathspace_satisfied_index on next_pathspace using btree (satisfied);
    create index next_pathspace_is_cycle_index on next_pathspace using btree (is_cycle);

    -- Populate initial pathspace with the primer query
    execute primer_query;

    -- Iterate until either we reach a depth limit or if there are satisfied paths
    while depth < max_depth and not exists(select 1 from next_pathspace np where np.satisfied)
        loop
            -- Rename tables to swap in the next pathspace as the current pathspace for the next traversal step
            alter table pathspace
                rename to pathspace_old;
            alter table next_pathspace
                rename to pathspace;
            alter table pathspace_old
                rename to next_pathspace;

            -- Clear the next pathspace scratch
            truncate table next_pathspace;

            -- Remove any non-satisfied terminals and cycles from pathspace to prune any terminal, non-satisfied paths
            delete
            from pathspace p
            where p.is_cycle
               or not exists(select 1 from edge e where e.start_id = p.next_id);

            -- Increase the current depth and execute the recursive query
            depth := depth + 1;
            execute recursive_query;
        end loop;

    -- Return all satisfied paths from the next pathspace table
    return query select *
                 from next_pathspace np
                 where np.satisfied;

    -- Close the result set
    return;
end;
$$
    language plpgsql volatile
                     strict;
Expansion using the ASP Harness

Take for consideration the following openCypher query: match p = allShortestPaths((s:NodeKind1)-[*..]->()) return p

The table record shape of the asp_harness function matches the recursive CTE traversal's record shape, meaning the result alias for this expansion can be dropped into a chain of expansions with minimal result set processing. As such the result alias record shape for an all shortest paths query is declared no differently than a recursive CTE traversal:

with ex0(root_id, next_id, depth, satisfied, is_cycle, path)

The harness function is then called. The first two parameters are expected to be Text values that contain reified statements that represent the primer and recursive queries for the traversal expansion. In the test case being explored, these parameter expectations are encoded in the test case header:

-- pgsql_params: {"pi0":"insert into next_pathspace (root_id, next_id, depth, satisfied, is_cycle, path) select e0.start_id, e0.end_id, 1, exists (select 1 from edge e0 where n1.id = e0.start_id), e0.start_id = e0.end_id, array [e0.id] from edge e0 join node n0 on n0.kind_ids operator (pg_catalog.&&) array [1]::int2[] and n0.id = e0.start_id join node n1 on n1.id = e0.end_id;", "pi1":"insert into next_pathspace (root_id, next_id, depth, satisfied, is_cycle, path) select ex0.root_id, e0.end_id, ex0.depth + 1, exists (select 1 from edge e0 where n1.id = e0.start_id), e0.id = any (ex0.path), ex0.path || e0.id from pathspace ex0 join edge e0 on e0.start_id = ex0.next_id join node n1 on n1.id = e0.end_id where ex0.depth < 5 and not ex0.is_cycle;"}

For this translation the primer query will be formated as the pi0 parameter:

insert into next_pathspace (root_id, next_id, depth, satisfied, is_cycle, path)
select e0.start_id,
       e0.end_id,
       1,
       exists (select 1 from edge e0 where n1.id = e0.start_id),
       e0.start_id = e0.end_id,
       array [e0.id]
from edge e0
         join node n0 on n0.kind_ids operator (pg_catalog.&&) array [1]::int2[] and n0.id = e0.start_id
         join node n1 on n1.id = e0.end_id;

The translation then authors the recursive query formatted as the pi1 parameter:

insert into next_pathspace (root_id, next_id, depth, satisfied, is_cycle, path)
select ex0.root_id,
       e0.end_id,
       ex0.depth + 1,
       exists (select 1 from edge e0 where n1.id = e0.start_id),
       e0.id = any (ex0.path),
       ex0.path || e0.id
from pathspace ex0
         join edge e0 on e0.start_id = ex0.next_id
         join node n1 on n1.id = e0.end_id
where ex0.depth < 5
  and not ex0.is_cycle;

The resulting output of the result alias s0 is then populated from the pathspace table:

select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0,
       (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite)
        from edge e0
        where e0.id = any (ex0.path))                     as e0,
       ex0.path                                           as ep0,
       (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1
from ex0
         join edge e0 on e0.id = any (ex0.path)
         join node n0 on n0.id = ex0.root_id
         join node n1 on e0.id = ex0.path[array_length(ex0.path, 1)::int4] and n1.id = e0.end_id

This binds the following additional components to query scope:

Component Type Usage
n0 nodecomposite Root node of each recursively expanded path.
e0 edgecomposite[] edgecomposite array of edges
ep0 int4[] Int4 array of edge identifiers
n1 nodecomposite Terminal node of each recursively expanded path.

Lastly, since this query is to return the traversed paths, the result variable ep0 is passed to a function edges_to_path to create the expected return record shape:

select edges_to_path(variadic ep0)::pathcomposite as p
from s0;

The complete translation is:

-- match p = allShortestPaths((s:NodeKind1)-[*..]->()) return p
with s0 as (with ex0(root_id, next_id, depth, satisfied, is_cycle, path)
                     as (select * from asp_harness(@pi0::text, @pi1::text, 5))
            select (n0.id, n0.kind_ids, n0.properties)::nodecomposite as n0,
                   (select array_agg((e0.id, e0.start_id, e0.end_id, e0.kind_id, e0.properties)::edgecomposite)
                    from edge e0
                    where e0.id = any (ex0.path))                     as e0,
                   ex0.path                                           as ep0,
                   (n1.id, n1.kind_ids, n1.properties)::nodecomposite as n1
            from ex0
                     join edge e0 on e0.id = any (ex0.path)
                     join node n0 on n0.id = ex0.root_id
                     join node n1 on e0.id = ex0.path[array_length(ex0.path, 1)::int4] and n1.id = e0.end_id)
select edges_to_path(variadic ep0)::pathcomposite as p
from s0;

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrUnsupportedExpansionDirection = errors.New("unsupported expansion direction")
)

Functions

func ConjoinExpressions

func ConjoinExpressions(kindMapper *contextAwareKindMapper, expressions []pgsql.Expression) (pgsql.Expression, error)

func ExtractSyntaxNodeReferences

func ExtractSyntaxNodeReferences(root pgsql.SyntaxNode) (*pgsql.IdentifierSet, error)

func FromCypher

func FromCypher(ctx context.Context, regularQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, stripLiterals bool) (format.Formatted, error)

func GetAggregatedFunctionParameterSymbols

func GetAggregatedFunctionParameterSymbols(call pgsql.FunctionCall) (*pgsql.SymbolTable, error)

func GetTypeHint

func GetTypeHint(expression pgsql.Expression) (pgsql.DataType, bool)

func InferExpressionType

func InferExpressionType(expression pgsql.Expression) (pgsql.DataType, error)

func MeasureSelectivity

func MeasureSelectivity(scope *Scope, owningIdentifierBound bool, expression pgsql.Expression) (int, error)

MeasureSelectivity attempts to measure how selective (i.e. how narrow) the query expression passed in is. This is a simple heuristic that is best-effort for attempting to find which side of a traversal step ()-[]->() is more selective.

The boolean parameter owningIdentifierBound is intended to represent if the identifier the expression constraints is part of a materialized set of nodes where the entity IDs of each are known at time of query. In this case the bound component is considered to be highly-selective.

Many numbers are magic values selected based on implementor's perception of selectivity of certain operators.

func RewriteFrameBindings

func RewriteFrameBindings(scope *Scope, expression pgsql.Expression) error

func SymbolsFor

func SymbolsFor(node pgsql.SyntaxNode) (*pgsql.SymbolTable, error)

func Translated

func Translated(translation Result) (string, error)

func TypeCastExpression

func TypeCastExpression(expression pgsql.Expression, dataType pgsql.DataType) (pgsql.Expression, error)

Types

type BindingResult

type BindingResult struct {
	Binding      *BoundIdentifier
	AlreadyBound bool
}

type BoundIdentifier

type BoundIdentifier struct {
	Identifier     pgsql.Identifier
	Alias          models.Optional[pgsql.Identifier]
	Parameter      *pgsql.Parameter
	LastProjection *Frame
	Dependencies   []*BoundIdentifier
	DataType       pgsql.DataType
}

BoundIdentifier is a declared query identifier bound to the current scope frame.

Bound identifiers have two states:

  • Defined - the translation code is aware of this identifier and its type
  • Visible - the identifier has been projected into the query's scope and can be referenced

Bound identifiers may also be aliased if the source query contains an alias for the identifier. In the openCypher query `match (n) return n as e` the projection for `n` is aliased as `e`. The translations will eagerly bind anonymous identifiers for traversal steps and rebind existing identifiers and their aliases to prevent naming collisions.

func (*BoundIdentifier) Aliased

func (s *BoundIdentifier) Aliased() pgsql.Identifier

func (*BoundIdentifier) Copy

func (s *BoundIdentifier) Copy() *BoundIdentifier

func (*BoundIdentifier) Dematerialize

func (s *BoundIdentifier) Dematerialize()

func (*BoundIdentifier) DependOn

func (s *BoundIdentifier) DependOn(other *BoundIdentifier)

func (*BoundIdentifier) MaterializedBy

func (s *BoundIdentifier) MaterializedBy(frame *Frame)

type BoundProjections

type BoundProjections struct {
	Items    pgsql.Projection
	Bindings []*BoundIdentifier
}

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

func NewExpressionTreeBuilder

func NewExpressionTreeBuilder() *Builder

func (*Builder) Depth

func (s *Builder) Depth() int

func (*Builder) IsEmpty

func (s *Builder) IsEmpty() bool

func (*Builder) PeekOperand

func (s *Builder) PeekOperand() pgsql.Expression

func (*Builder) PopOperand

func (s *Builder) PopOperand(kindMapper *contextAwareKindMapper) (pgsql.Expression, error)

func (*Builder) PushOperand

func (s *Builder) PushOperand(operand pgsql.Expression)

type Constraint

type Constraint struct {
	Dependencies *pgsql.IdentifierSet
	Expression   pgsql.Expression
}

Constraint is an extracted expression that contains an identifier set of symbols required to be in scope for this constraint to be solvable.

type ConstraintTracker

type ConstraintTracker struct {
	Constraints []*Constraint
}

ConstraintTracker is a tool for associating constraints (e.g. binary or unary expressions that constrain a set of identifiers) with the identifier set they constrain.

This is useful for rewriting a where-clause so that conjoined components can be isolated:

Where Clause:

where a.name = 'a' and b.name = 'b' and c.name = 'c' and a.num_a > 1 and a.ef = b.ef + c.ef

Isolated Constraints:

"a":           a.name = 'a' and a.num_a > 1
"b":           b.name = 'b'
"c":           c.name = 'c'
"a", "b", "c": a.ef = b.ef + c.ef

func NewConstraintTracker

func NewConstraintTracker() *ConstraintTracker

func (*ConstraintTracker) Constrain

func (s *ConstraintTracker) Constrain(kindMapper *contextAwareKindMapper, dependencies *pgsql.IdentifierSet, constraintExpression pgsql.Expression) error

func (*ConstraintTracker) ConsumeAll

func (s *ConstraintTracker) ConsumeAll(kindMapper *contextAwareKindMapper) (*Constraint, error)

func (*ConstraintTracker) ConsumeSet

func (s *ConstraintTracker) ConsumeSet(kindMapper *contextAwareKindMapper, visible *pgsql.IdentifierSet) (*Constraint, error)

ConsumeSet takes a given scope (a set of identifiers considered in-scope) and locates all constraints that can be satisfied by the scope's identifiers.

```

visible := pgsql.IdentifierSet{
	"a": struct{}{},
	"b": struct{}{},
}

tracker := ConstraintTracker{
	Constraints: []*Constraint{{
		Dependencies: pgsql.IdentifierSet{
			"a": struct{}{},
		},
		Expression: &pgsql.BinaryExpression{
			Operator: pgsql.OperatorEquals,
			LOperand: pgsql.CompoundIdentifier{"a", "name"},
			ROperand: pgsql.Literal{
				Value: "a",
			},
		},
	}},
}

satisfiedConstraints, expression := tracker.ConsumeSet(visible)

```

func (*ConstraintTracker) HasConstraints

func (s *ConstraintTracker) HasConstraints(scope *pgsql.IdentifierSet) (bool, error)

type Delete

type Delete struct {
	Frame         *Frame
	TargetBinding *BoundIdentifier
	UpdateBinding *BoundIdentifier
}

type Expansion

type Expansion struct {
	Frame       *Frame
	PathBinding *BoundIdentifier
	Options     ExpansionOptions

	PrimerNodeConstraints              pgsql.Expression
	PrimerNodeSatisfactionProjection   pgsql.SelectItem
	PrimerNodeJoinCondition            pgsql.Expression
	EdgeConstraints                    pgsql.Expression
	EdgeJoinCondition                  pgsql.Expression
	RecursiveConstraints               pgsql.Expression
	ExpansionNodeJoinCondition         pgsql.Expression
	TerminalNodeConstraints            pgsql.Expression
	TerminalNodeSatisfactionProjection pgsql.SelectItem

	PrimerQueryParameter            *BoundIdentifier
	BackwardPrimerQueryParameter    *BoundIdentifier
	RecursiveQueryParameter         *BoundIdentifier
	BackwardRecursiveQueryParameter *BoundIdentifier

	EdgeStartIdentifier pgsql.Identifier
	EdgeStartColumn     pgsql.CompoundIdentifier
	EdgeEndIdentifier   pgsql.Identifier
	EdgeEndColumn       pgsql.CompoundIdentifier

	Projection []pgsql.SelectItem
}

func NewExpansionModel

func NewExpansionModel(part *PatternPart, relationshipPattern *cypher.RelationshipPattern) *Expansion

func (*Expansion) CanExecuteBidirectionalSearch

func (s *Expansion) CanExecuteBidirectionalSearch() bool

func (*Expansion) CompletePattern

func (s *Expansion) CompletePattern(traversalStep *TraversalStep) error

func (*Expansion) FlipDirection

func (s *Expansion) FlipDirection()

type ExpansionBuilder

type ExpansionBuilder struct {
	PrimerStatement     pgsql.Select
	RecursiveStatement  pgsql.Select
	ProjectionStatement pgsql.Select
	// contains filtered or unexported fields
}

func NewExpansionBuilder

func NewExpansionBuilder(queryParameters map[string]any, traversalStep *TraversalStep) (*ExpansionBuilder, error)

func (*ExpansionBuilder) Build

func (s *ExpansionBuilder) Build(expansionIdentifier pgsql.Identifier) pgsql.Query

func (*ExpansionBuilder) BuildAllShortestPathsRoot

func (s *ExpansionBuilder) BuildAllShortestPathsRoot() (pgsql.Query, error)

func (*ExpansionBuilder) BuildBiDirectionalAllShortestPathsRoot

func (s *ExpansionBuilder) BuildBiDirectionalAllShortestPathsRoot() (pgsql.Query, error)

func (*ExpansionBuilder) BuildShortestPathsRoot

func (s *ExpansionBuilder) BuildShortestPathsRoot() (pgsql.Query, error)

type ExpansionOptions added in v0.3.1

type ExpansionOptions struct {
	FindShortestPath     bool
	FindAllShortestPaths bool
	MinDepth             models.Optional[int64]
	MaxDepth             models.Optional[int64]
}

type ExpressionTreeTranslator

type ExpressionTreeTranslator struct {
	UserConstraints        *ConstraintTracker
	TranslationConstraints *ConstraintTracker
	// contains filtered or unexported fields
}

func NewExpressionTreeTranslator

func NewExpressionTreeTranslator(kindMapper *contextAwareKindMapper) *ExpressionTreeTranslator

func (*ExpressionTreeTranslator) AddTranslationConstraint

func (s *ExpressionTreeTranslator) AddTranslationConstraint(requiredIdentifiers *pgsql.IdentifierSet, expression pgsql.Expression) error

func (*ExpressionTreeTranslator) AddUserConstraint

func (s *ExpressionTreeTranslator) AddUserConstraint(requiredIdentifiers *pgsql.IdentifierSet, expression pgsql.Expression) error

func (*ExpressionTreeTranslator) CompleteBinaryExpression

func (s *ExpressionTreeTranslator) CompleteBinaryExpression(scope *Scope, operator pgsql.Operator) error

func (*ExpressionTreeTranslator) ConstrainConjoinedOperandPair

func (s *ExpressionTreeTranslator) ConstrainConjoinedOperandPair() error

func (*ExpressionTreeTranslator) ConstrainDisjointOperandPair

func (s *ExpressionTreeTranslator) ConstrainDisjointOperandPair() error

func (*ExpressionTreeTranslator) ConsumeAllConstraints

func (s *ExpressionTreeTranslator) ConsumeAllConstraints() (*Constraint, error)

func (*ExpressionTreeTranslator) ConsumeConstraintsFromVisibleSet

func (s *ExpressionTreeTranslator) ConsumeConstraintsFromVisibleSet(visible *pgsql.IdentifierSet) (*Constraint, error)

func (*ExpressionTreeTranslator) PeekOperand

func (s *ExpressionTreeTranslator) PeekOperand() pgsql.Expression

func (*ExpressionTreeTranslator) PopBinaryExpression

func (s *ExpressionTreeTranslator) PopBinaryExpression(operator pgsql.Operator) (*pgsql.BinaryExpression, error)

func (*ExpressionTreeTranslator) PopOperand

func (s *ExpressionTreeTranslator) PopOperand() (pgsql.Expression, error)

func (*ExpressionTreeTranslator) PopParenthetical

func (s *ExpressionTreeTranslator) PopParenthetical() (*pgsql.Parenthetical, error)

func (*ExpressionTreeTranslator) PopRemainingExpressionsAsUserConstraints

func (s *ExpressionTreeTranslator) PopRemainingExpressionsAsUserConstraints() error

func (*ExpressionTreeTranslator) PushOperand

func (s *ExpressionTreeTranslator) PushOperand(expression pgsql.Expression)

func (*ExpressionTreeTranslator) PushParenthetical

func (s *ExpressionTreeTranslator) PushParenthetical()

func (*ExpressionTreeTranslator) VisitOperator

func (s *ExpressionTreeTranslator) VisitOperator(operator pgsql.Operator)

type Frame

type Frame struct {
	Previous *Frame
	Binding  *BoundIdentifier
	Visible  *pgsql.IdentifierSet

	Exported *pgsql.IdentifierSet
	// contains filtered or unexported fields
}

Frame represents a snapshot of all identifiers defined and visible in a given scope

func (*Frame) Export

func (s *Frame) Export(identifier pgsql.Identifier)

func (*Frame) Known

func (s *Frame) Known() *pgsql.IdentifierSet

func (*Frame) RestoreStashed

func (s *Frame) RestoreStashed()

func (*Frame) Reveal

func (s *Frame) Reveal(identifier pgsql.Identifier)

func (*Frame) Stash

func (s *Frame) Stash(identifier pgsql.Identifier)

func (*Frame) Unexport

func (s *Frame) Unexport(identifer pgsql.Identifier)

type FrameBindingRewriter

type FrameBindingRewriter struct {
	walk.Visitor[pgsql.SyntaxNode]
	// contains filtered or unexported fields
}

func (*FrameBindingRewriter) Enter

func (s *FrameBindingRewriter) Enter(node pgsql.SyntaxNode)

func (*FrameBindingRewriter) Exit

func (s *FrameBindingRewriter) Exit(node pgsql.SyntaxNode)

type IdentifierGenerator

type IdentifierGenerator map[pgsql.DataType]int

IdentifierGenerator is a map that creates a unique identifier for each call with a given data type. This ensures that renamed identifiers in queries do not conflict with each other.

func NewIdentifierGenerator

func NewIdentifierGenerator() IdentifierGenerator

func (IdentifierGenerator) NewIdentifier

func (s IdentifierGenerator) NewIdentifier(dataType pgsql.DataType) (pgsql.Identifier, error)

type LabelAssignment

type LabelAssignment struct {
	Kinds pgsql.Expression
}

type Mutations

type Mutations struct {
	Deletions *graph.IndexedSlice[pgsql.Identifier, *Delete]
	Updates   *graph.IndexedSlice[pgsql.Identifier, *Update]
}

func NewMutations

func NewMutations() *Mutations

func (*Mutations) AddDeletion

func (s *Mutations) AddDeletion(scope *Scope, targetIdentifier pgsql.Identifier, frame *Frame) (*Delete, error)

func (*Mutations) AddKindAssignment

func (s *Mutations) AddKindAssignment(scope *Scope, targetIdentifier pgsql.Identifier, kinds graph.Kinds) error

func (*Mutations) AddKindRemoval

func (s *Mutations) AddKindRemoval(scope *Scope, targetIdentifier pgsql.Identifier, kinds graph.Kinds) error

func (*Mutations) AddPropertyAssignment

func (s *Mutations) AddPropertyAssignment(scope *Scope, propertyLookup PropertyLookup, operator pgsql.Operator, assignmentValueExpression pgsql.Expression) error

func (*Mutations) AddPropertyRemoval

func (s *Mutations) AddPropertyRemoval(scope *Scope, propertyLookup PropertyLookup) error

type NodeSelect

type NodeSelect struct {
	Frame       *Frame
	Binding     *BoundIdentifier
	Select      pgsql.Select
	Constraints pgsql.Expression
}

type Pattern

type Pattern struct {
	Parts []*PatternPart
}

func (*Pattern) CurrentPart

func (s *Pattern) CurrentPart() *PatternPart

func (*Pattern) NewPart

func (s *Pattern) NewPart() *PatternPart

func (*Pattern) Reset

func (s *Pattern) Reset()

type PatternConstraints

type PatternConstraints struct {
	LeftNode  *Constraint
	Edge      *Constraint
	RightNode *Constraint
}

PatternConstraints is a struct that represents all constraints that can be solved during a traversal step pattern: `()-[]->()`.

func (*PatternConstraints) FlipNodes

func (s *PatternConstraints) FlipNodes()

func (*PatternConstraints) OptimizePatternConstraintBalance

func (s *PatternConstraints) OptimizePatternConstraintBalance(scope *Scope, traversalStep *TraversalStep) error

OptimizePatternConstraintBalance considers the constraints that apply to a pattern segment's bound identifiers.

If only the right side of the pattern segment is constrained, this could result in an imbalanced expansion where one side of the traversal has an extreme disparity in search space.

In cases that match this heuristic, it's beneficial to begin the traversal with the most tightly constrained set of nodes. To accomplish this we flip the order of the traversal step.

type PatternPart

type PatternPart struct {
	IsTraversal      bool
	ShortestPath     bool
	AllShortestPaths bool
	PatternBinding   *BoundIdentifier
	TraversalSteps   []*TraversalStep
	NodeSelect       NodeSelect
	Constraints      *ConstraintTracker
}

func (*PatternPart) ContainsExpansions

func (s *PatternPart) ContainsExpansions() bool

func (*PatternPart) LastStep

func (s *PatternPart) LastStep() *TraversalStep

type Projection

type Projection struct {
	SelectItem pgsql.SelectItem
	Alias      models.Optional[pgsql.Identifier]
}

func (*Projection) SetAlias

func (s *Projection) SetAlias(alias pgsql.Identifier)

func (*Projection) SetIdentifier

func (s *Projection) SetIdentifier(identifier pgsql.Identifier)

type Projections

type Projections struct {
	Distinct    bool
	Frame       *Frame
	Constraints pgsql.Expression
	Items       []*Projection
	GroupBy     []pgsql.SelectItem
}

func (*Projections) Add

func (s *Projections) Add(projection *Projection)

func (*Projections) Current

func (s *Projections) Current() *Projection

type PropertyAssignment

type PropertyAssignment struct {
	Field           string
	Operator        pgsql.Operator
	ValueExpression pgsql.Expression
}

type PropertyLookup

type PropertyLookup struct {
	Reference pgsql.CompoundIdentifier
	Field     string
}

type Query

type Query struct {
	Parts []*QueryPart
}

func (*Query) AddPart

func (s *Query) AddPart(part *QueryPart)

func (*Query) CurrentPart

func (s *Query) CurrentPart() *QueryPart

func (*Query) HasParts

func (s *Query) HasParts() bool

type QueryPart

type QueryPart struct {
	Model     *pgsql.Query
	Frame     *Frame
	Updates   []*Mutations
	SortItems []*pgsql.OrderBy
	Skip      pgsql.Expression
	Limit     pgsql.Expression
	// contains filtered or unexported fields
}

func NewQueryPart

func NewQueryPart(numReadingClauses, numUpdatingClauses int) *QueryPart

func (*QueryPart) AddFromClause

func (s *QueryPart) AddFromClause(clause pgsql.FromClause)

func (*QueryPart) AddPatternPredicateFuture

func (s *QueryPart) AddPatternPredicateFuture(predicateFuture *pgsql.Future[*Pattern])

func (*QueryPart) AddProperty

func (s *QueryPart) AddProperty(key string, expression pgsql.Expression)

func (*QueryPart) ConsumeCurrentPattern

func (s *QueryPart) ConsumeCurrentPattern() *Pattern

func (*QueryPart) ConsumeFromClauses

func (s *QueryPart) ConsumeFromClauses() []pgsql.FromClause

func (*QueryPart) ConsumeProperties

func (s *QueryPart) ConsumeProperties() map[string]pgsql.Expression

func (*QueryPart) CurrentOrderBy

func (s *QueryPart) CurrentOrderBy() *pgsql.OrderBy

func (*QueryPart) CurrentProjection

func (s *QueryPart) CurrentProjection() *Projection

func (*QueryPart) HasDeletions

func (s *QueryPart) HasDeletions() bool

func (*QueryPart) HasMutations

func (s *QueryPart) HasMutations() bool

func (*QueryPart) HasProjections

func (s *QueryPart) HasProjections() bool

func (*QueryPart) HasProperties

func (s *QueryPart) HasProperties() bool

func (*QueryPart) PrepareMutations

func (s *QueryPart) PrepareMutations()

func (*QueryPart) PrepareProjection

func (s *QueryPart) PrepareProjection()

func (*QueryPart) PrepareProjections

func (s *QueryPart) PrepareProjections(distinct bool)

func (*QueryPart) RestoreStashedPattern

func (s *QueryPart) RestoreStashedPattern()

func (*QueryPart) StashCurrentPattern

func (s *QueryPart) StashCurrentPattern()

type Removal

type Removal struct {
	Field string
}

type Result

type Result struct {
	Statement  pgsql.Statement
	Parameters map[string]any
}

func Translate

func Translate(ctx context.Context, cypherQuery *cypher.RegularQuery, kindMapper pgsql.KindMapper, parameters map[string]any) (Result, error)

type Scope

type Scope struct {
	// contains filtered or unexported fields
}

Scope contains all identifier definitions and their temporal resolutions in a []*Frame field.

Frames may be pushed onto the stack, advancing the scope of the query to the next component. Frames may be popped from the stack, rewinding visibility to an earlier temporal state. This is useful when navigating subqueries and nested expressions that require their own descendent scope lifecycle.

Each frame is associated with an identifier that represents the query AST element that contains all visible projections. This is required when disambiguating references that otherwise belong to a frame.

func NewScope

func NewScope() *Scope

func (*Scope) Alias

func (s *Scope) Alias(alias pgsql.Identifier, binding *BoundIdentifier)

func (*Scope) AliasedLookup

func (s *Scope) AliasedLookup(identifier pgsql.Identifier) (*BoundIdentifier, bool)

func (*Scope) CurrentFrame

func (s *Scope) CurrentFrame() *Frame

func (*Scope) CurrentFrameBinding

func (s *Scope) CurrentFrameBinding() *BoundIdentifier

func (*Scope) Declare

func (s *Scope) Declare(identifier pgsql.Identifier)

func (*Scope) Define

func (s *Scope) Define(identifier pgsql.Identifier, dataType pgsql.DataType) *BoundIdentifier

func (*Scope) DefineNew

func (s *Scope) DefineNew(dataType pgsql.DataType) (*BoundIdentifier, error)

func (*Scope) FrameAt

func (s *Scope) FrameAt(depth int) *Frame

func (*Scope) IsMaterialized

func (s *Scope) IsMaterialized(identifier pgsql.Identifier) bool

func (*Scope) Lookup

func (s *Scope) Lookup(identifier pgsql.Identifier) (*BoundIdentifier, bool)

func (*Scope) LookupBindings

func (s *Scope) LookupBindings(identifiers ...pgsql.Identifier) ([]*BoundIdentifier, error)

func (*Scope) LookupString

func (s *Scope) LookupString(identifierString string) (*BoundIdentifier, bool)

func (*Scope) PopFrame

func (s *Scope) PopFrame() error

func (*Scope) PreviousFrame

func (s *Scope) PreviousFrame() *Frame

func (*Scope) PruneDefinitions

func (s *Scope) PruneDefinitions(protectedIdentifiers *pgsql.IdentifierSet) error

func (*Scope) PushFrame

func (s *Scope) PushFrame() (*Frame, error)

func (*Scope) ReferenceFrame

func (s *Scope) ReferenceFrame() *Frame

func (*Scope) Snapshot

func (s *Scope) Snapshot() *Scope

func (*Scope) UnwindToFrame

func (s *Scope) UnwindToFrame(frame *Frame) error

func (*Scope) Visible

func (s *Scope) Visible() *pgsql.IdentifierSet

type Translator

type Translator struct {
	walk.Visitor[cypher.SyntaxNode]
	// contains filtered or unexported fields
}

func NewTranslator

func NewTranslator(ctx context.Context, kindMapper pgsql.KindMapper, parameters map[string]any) *Translator

func (*Translator) Enter

func (s *Translator) Enter(expression cypher.SyntaxNode)

func (*Translator) Exit

func (s *Translator) Exit(expression cypher.SyntaxNode)

type TraversalStep

type TraversalStep struct {
	Frame                  *Frame
	Direction              graph.Direction
	Expansion              *Expansion
	LeftNode               *BoundIdentifier
	LeftNodeBound          bool
	LeftNodeConstraints    pgsql.Expression
	LeftNodeJoinCondition  pgsql.Expression
	Edge                   *BoundIdentifier
	EdgeConstraints        *Constraint
	EdgeJoinCondition      pgsql.Expression
	RightNode              *BoundIdentifier
	RightNodeBound         bool
	RightNodeConstraints   pgsql.Expression
	RightNodeJoinCondition pgsql.Expression
	Projection             []pgsql.SelectItem
}

func (*TraversalStep) EndNode

func (s *TraversalStep) EndNode() (*BoundIdentifier, error)

EndNode will find the terminal node of this pattern segment based on the segment's direction

func (*TraversalStep) FlipNodes

func (s *TraversalStep) FlipNodes()

func (*TraversalStep) StartNode

func (s *TraversalStep) StartNode() (*BoundIdentifier, error)

StartNode will find the root node of this pattern segment based on the segment's direction

type Update

type Update struct {
	Frame               *Frame
	JoinConstraint      pgsql.Expression
	Projection          []pgsql.SelectItem
	TargetBinding       *BoundIdentifier
	UpdateBinding       *BoundIdentifier
	Removals            *graph.IndexedSlice[string, Removal]
	PropertyAssignments *graph.IndexedSlice[string, PropertyAssignment]
	KindRemovals        graph.Kinds
	KindAssignments     graph.Kinds
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL