GithubHelp home page GithubHelp logo

neo4j-contrib / cypher-dsl Goto Github PK

View Code? Open in Web Editor NEW
188.0 27.0 63.0 109.93 MB

A Java DSL (Builder) for the Cypher Query Language

Home Page: http://neo4j-contrib.github.io/cypher-dsl

License: Apache License 2.0

Java 97.55% Shell 0.04% Cypher 2.38% Smarty 0.02%
dsl java-dsl java neo4j cypher cypher-query-language

cypher-dsl's Introduction

The Neo4j Cypher-DSL

badge measure?project=org measure?project=org Maven Central

The Neo4j Cypher-DSL in its current form is a spin-off from Spring Data Neo4j 6+ (né Spring Data Neo4j⚡️RX), where it is used to generate all Cypher queries. We thank all contributors to all branches prior to 2020.0 for their effort in creating the previous versions.

The primary goal of this project is to have a type safe way of creating Cypher queries targeted at Neo4j 4.0+. Most of the constructs used here are modelled after openCypher, but we include several constructs specific to Neo4j.

The core module of the Neo4j Cypher-DSL has no required runtime dependencies.

Versioning

This rebooted version of the Neo4j Cypher-DSL uses CalVer in the same way Spring does since early 2020 (see Updates to Spring Versions), starting at 2020.0.0.

The year digit is treated in a semver fashion for the core module. That means that you won’t experience any breaking changes when staying in a release line. However, the first or the latest release in a given year does not necessarily reflect the current calendar year. It’s kind of a trade-off, but one that we think is valuable, otherwise we would need to postpone the first release in a year until we need todo some breaking changes.

Manual

For a gentle introduction and some getting started guides, please use our Manual.

Getting Started

Adding the necessary dependencies

First, include the dependency to the Neo4j Cypher-DSL under the following coordinates: org.neo4j:neo4j-cypher-dsl:

Maven configuration

Inclusion of the Neo4j Cypher-DSL in a Maven project
<dependency>
	<groupId>org.neo4j</groupId>
	<artifactId>neo4j-cypher-dsl</artifactId>
	<version>2024.0.1</version>
</dependency>

Gradle configuration

Inclusion of the Neo4j Cypher-DSL in a Gradle project
dependencies {
    compile 'org.neo4j:neo4j-cypher-dsl:2024.0.1'
}

A simple example

With the Cypher-DSL, you can build your queries starting with the static methods provided through org.neo4j.cypherdsl.core.Cypher. Static imports for those packages should be allowed:

import static org.neo4j.cypherdsl.core.Cypher.*;

import org.neo4j.cypherdsl.core.Cypher;

class SimpleExample {

    public static void main(String... a) {

        var m = node("Movie").named("m");
        var statement = Cypher.match(m)
            .returning(m)
            .build();

        System.out.println(statement.getCypher());
        // Prints MATCH (m:`Movie`) RETURN m
    }
}

Required Java Version

From version 2023.0.0 onwards, the minimal required Java version to use and build the Cypher-DSL is Java 17. If you need a version that is compatible with Java 8, please use 2022.8.x.

Licensing

The Cypher-DSL itself is licenced under the Apache License 2.0.

cypher-dsl's People

Contributors

aakashsorathiya avatar aldrinm avatar andy2003 avatar davided avatar dependabot[bot] avatar fbiville avatar hindog avatar ikwattro avatar jexp avatar meistermeier avatar michael-simons avatar nawroth avatar ractive avatar rickardoberg avatar romain-rossi avatar simpsonjulian avatar systay avatar tbaum avatar thephil avatar timmystorms avatar treo avatar wgorder avatar wouterv avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cypher-dsl's Issues

Add support for Index and Range results

Currently it is not possible to model queries like:

RETURN range(0, 10)[3]

or

RETURN range(0, 10)[..3]

or

RETURN range(0, 10)[-3..]

or

RETURN range(0, 10)[2..4]

or

MATCH (person: Person) RETURN person { livesIn: [(person)-[: LIVES_IN]->(personLivesIn: Location) | personLivesIn { .name }][0] } 

multiple WhereClause are merged, but not WithClause

if we add multiple WhereClause to a Query the new additions are merged with the previous WhereClause to create a compound. This doesn't happen for WithClause which I can't determine any rationale behind. There may also be other clauses that could usefully be merged.

Suggestion for implementing is to change check in Query.add to use instanceOf MergeClause instead of instanceof WhereClause and create MergeClause with the single merge method. Happy to provide pull request if helpful

Regex support and optional operator (!) broken

Two bugs in cypher-dsl:

  • Neo4j does not use the slash as a regexp delimiter, which is still used by cypher-dsl.

  • Cypher dsl generates queries with the ! operator like WHERE n.prop!="foo" which is now treated as an error in cypher:

    Error: Cypher does not support != for inequality comparisons. It's used for nullable properties instead. You probably meant <> instead. Read more about this in the operators chapter in the manual.

    Adding a space fixes this problem: WHERE n.prop! ="foo"

Release version 2.0.2

Hi Michael,

Current snapshot of version 2.0.2 add quite a useful feature of using labels. This is something that I want to make use of but in order to do so I have to copy Cypher-DSL to my project with a different version label so that it can be built and installed.
Please release version 2.0.2.

Thank you,
Alex

Javadoc

Please provide proper Javadoc for all public APIs, esp. within the org.neo4j.cypherdsl.query package.

missing pom dependencies

including the latest stable version by using

    <dependency>
        <groupId>org.neo4j</groupId>
        <artifactId>neo4j-cypher-dsl</artifactId>
        <version>1.6</version>
    </dependency>

it failes in missing dependencies

  java.lang.NoClassDefFoundError: com/mysema/query/types/Path

the dependency

    <dependency>
        <groupId>com.mysema.querydsl</groupId>
        <artifactId>querydsl-core</artifactId>
        <version>2.2.3</version>
    </dependency>

should not be optional !!

Cypher DSL (Java) is not compatible with Scala 2.10.0

When trying to execute a Cypher query 1.8.0_M06 web console, the following exception puts an end to the attempt:

2012-08-14 19:33:12,597 [2138463779@qtp-816829837-2] ERROR org.mortbay.log - /db/data/cypher
java.lang.NoSuchMethodError: scala.Predef$.augmentString(Ljava/lang/String;)Lscala/collection/immutable/StringOps;
at org.neo4j.cypher.CypherParser.(CypherParser.scala:27) ~[neo4j-cypher-1.8.M06.jar:1.8.M06]
at org.neo4j.cypher.CypherParser.(CypherParser.scala:25) ~[neo4j-cypher-1.8.M06.jar:1.8.M06]
at org.neo4j.cypher.ExecutionEngine.createCorrectParser(ExecutionEngine.scala:47) ~[neo4j-cypher-1.8.M06.jar:1.8.M06]
at org.neo4j.cypher.ExecutionEngine.(ExecutionEngine.scala:37) ~[neo4j-cypher-1.8.M06.jar:1.8.M06]
at org.neo4j.cypher.javacompat.ExecutionEngine.(ExecutionEngine.java:41) ~[neo4j-cypher-1.8.M06.jar:1.8.M06]
at org.neo4j.server.rest.web.CypherService.(CypherService.java:50) ~[neo4j-server-1.8.M06.jar:1.8.M06]
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.7.0_04]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57) ~[na:1.7.0_04]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.7.0_04]
at java.lang.reflect.Constructor.newInstance(Constructor.java:525) ~[na:1.7.0_04]
at com.sun.jersey.server.spi.component.ResourceComponentConstructor._construct(ResourceComponentConstructor.java:198) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.spi.component.ResourceComponentConstructor.construct(ResourceComponentConstructor.java:179) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.impl.resource.PerRequestFactory$PerRequest._getInstance(PerRequestFactory.java:182) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.impl.resource.PerRequestFactory$AbstractPerRequest.getInstance(PerRequestFactory.java:144) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.impl.application.WebApplicationContext.getResource(WebApplicationContext.java:238) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.impl.uri.rules.ResourceClassRule.accept(ResourceClassRule.java:83) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.impl.uri.rules.RightHandPathRule.accept(RightHandPathRule.java:147) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.impl.uri.rules.RootResourceClassesRule.accept(RootResourceClassesRule.java:84) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1469) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.impl.application.WebApplicationImpl._handleRequest(WebApplicationImpl.java:1400) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1349) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.server.impl.application.WebApplicationImpl.handleRequest(WebApplicationImpl.java:1339) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.spi.container.servlet.WebComponent.service(WebComponent.java:416) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:537) ~[jersey-server-1.9.jar:1.9]
at com.sun.jersey.spi.container.servlet.ServletContainer.service(ServletContainer.java:699) ~[jersey-server-1.9.jar:1.9]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:820) ~[servlet-api-2.5-20081211.jar:na]
at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511) ~[jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1166) ~[jetty-6.1.25.jar:6.1.25]
at org.neo4j.server.statistic.StatisticFilter.doFilter(StatisticFilter.java:62) ~[neo4j-server-1.8.M06.jar:1.8.M06]
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157) ~[jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388) ~[jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.handler.HandlerCollection.handle(HandlerCollection.java:114) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.Server.handle(Server.java:326) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:943) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:756) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:218) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.io.nio.SelectChannelEndPoint.run(SelectChannelEndPoint.java:410) [jetty-6.1.25.jar:6.1.25]
at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582) [jetty-util-6.1.25.jar:6.1.25]

Add support to create a point with an parameter

Currently there is no way to create a cypher query like:

MATCH (person:`Person`)
WHERE distance(person.location, point($point.point)) = $point.distance
WITH person
RETURN person { .name } AS person

calls to stored procedure

It's not possible to use statement builder's methods
org.neo4j.cypherdsl.core.Cypher#call + org.neo4j.cypherdsl.core.ExposesCall.ExposesWithArgs#withArgs

because "call" returns package-private interface org.neo4j.cypherdsl.core.ProcedureCall.OngoingStandaloneCallWithoutArguments

Example:

        Statement call = Cypher
                .call("dbms.listConfig")
                .withArgs(Cypher.literalOf("browser"))
                .yield("name")
                .build();

P.S.
Test org.neo4j.cypherdsl.core.ProcedureCallsIT#withArgsAndYield passes because of the same package

PPS
May be possible to use something like
org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingWithoutWhere as return to continue the builder chain ?

Automatically generate symbolic name

It would be convenient if there was an option or automatic feature to generate a random symbolic name, so that a node could be referred to multiple times in a query without needing to explicitly name it (i.e, reusing a variable of type Node)

        Node strawberry = node("Fruit", mapOf("kind", literalOf("strawberry")));
        RENDERER.render(match(strawberry).set(strawberry.property("color").to(literalOf("red"))));

Right now, this will fail, because a SymbolicName needs to be added to 'strawberry' for it to be able to be referenced in the 'set' clause. Instead of setting the name that will never actually need to be referenced by the author, it could be automatically set to a random UUID or something else so that the variable of type Node can be reused throughout the query

Automate release preparation and cleanup.

This includes

The snapshot version of the POM should be changed to something unrealistic which doesn't indicate a next release date or something.

The corresponding targets should than be called from Team City.

collect(distinct(identifier)) requires cast

The legal cypher query

collect(distinct("id"))

can't be written as

collect(distinct(identifier("id"))) 

without a cast to make it

collect((ScalarExpression)distinct(identifier("id"))))

Support labels in Set clause

Similar to the support just added in Remove. Allows for renaming of labels for example:

MATCH (n:oldLabel) SET n :newLabel REMOVE n:oldLabel

I will work on this when I get some time and submit a PR. This issue is so I don't lose track of it.

Relationship property doesn't get created

I've tried to get the following query to work, but without success:

start(nodesById("s", 1), nodesById("e", 2))
.match(path("path", node("s").both().hops(2, 6).node("e")))
.createUnique(node("s").both(identifier("NEAR")).as("r").node(identifier("e")))
.set(property(identifier("r").property("nearness"), avg(length(identifier("path")))))

gives:

CYPHER 1.8 
START s=node(1),e=node(2) 
MATCH path=(s)-[*2..6]-(e) 
CREATE UNIQUE (s)-[r:NEAR]-(e) 
SET r.nearness=avg(length(path))

What I'm trying to accomplish here, is to create a relationship between two given nodes. This relationship should contain a property "nearness" that represents the average length of all paths between the given nodes with a length between 2 and 6.
Although no exception is thrown, the property nearness isn't created and thus I suppose that "avg(length(path))" isn't evaluated. If I use a fixed value (e.g. r.nearness=1), the query works as expected.

Another side question. I'm using this in a the context of Spring Data Neo4j. When I try to retrieve a "NEAR" relationship, an exception is thrown stating that the "type" property could not be found. This makes sense because I'm using the ExcecutionEngine which is part of the native API. Is there a way that this property is automagically created, or should I do this myself (which actually works)?

Generating Q classes in gradle

i'm not very experienced in creating gradle scripts, would you please create sample gradle script for generating Q classes?

Predicate generation while having an string array over relation

I have an string array over relation and unable to generate predicate with querydsl.

Consider the following scenario,

NODE {id: 10} --- [LINKS_WITH {markets: ['US', 'France']}] ---> NODE {id: 21}
NODE {id: 10} --- [LINKS_WITH {markets: ['UK']}] ---> NODE {id: 22}
NODE {id: 10} --- [LINKS_WITH {markets: ['Canada']}] ---> NODE {id: 23}
NODE {id: 10} --- [LINKS_WITH {markets: ['Germany']}] ---> NODE {id: 24}

Now I have to get all nodes associated with the node having id 10 if the relation contains France in it.

I am unable to generate its predicate. Any help would be highly appreciated.

Support for "CREATE UNIQUE"

The RELATE command has been renamed to CREATE UNIQUE in cypher.
RELATE has been removed with #15 from cypher-dsl, but CREATE UNIQUE is not supported yet.

Not correct render for statement with call+withArgs

Example:

Renderer.getDefaultRenderer().render(
  .............
  .call("apoc.create.relationship")
  .withArgs(
          parent.getRequiredSymbolicName(),
          literalOf(type.name()),
          mapOf(
                  "score", literalOf(0.33),
                  "weight", literalOf(1.7)
          ),
          child.getRequiredSymbolicName()
  )
  .yield(createdRelationship)
  .............
)

where
parent - named org.neo4j.cypherdsl.core.Node
child - named org.neo4j.cypherdsl.core.Node
type - Enum
mapOf - org.neo4j.cypherdsl.core.Cypher#mapOf
literalOf - org.neo4j.cypherdsl.core.Cypher#literalOf

Renderer returns:

CALL apoc.create.relationship(parent, 'ChildEdge'{score: 0.33, weight: 1.7}child) YIELD rel

But expected:

CALL apoc.create.relationship(parent, 'ChildEdge', {score: 0.33, weight: 1.7}, child) YIELD rel

Upgrade to Querydsl 3.x

The current codebase is incompatible with Querydsl 3.x due to some incompatible changes in Querydsl. Currently this is blocking the Spring Data Neo4j snapshots from building as Spring Data has already upgraded to 3.x.

Optional match with multiple where

Cannot construct the following query:
MATCH (change:Change)-[codeRelation:CODE]-(code) WHERE id(code)=147 OPTIONAL MATCH (change)-[changeDetailsRelation:CHANGE_DETAILS]-(changeDetail) WHERE id(code)=147 RETURN change,codeRelation,changeDetailsRelation,changeDetail
Cannot put a 2nd match-clause after the first where.

CypherQueryDSL
	.match(node("change").label("Change")
		.both("CODE").as("codeRelation")
		.node("code"))
	.where(id("code").eq(codeId))
	.match(node("change").label("Change") // not allowed after where
		.both("CHANGE_DETAILS").as("changeDetailsRelation")
		.node("changeDetail"))
	.where(id("code").eq(codeId))
	.returns(identifiers("change", "codeRelation", "changeDetailsRelation", "changeDetail"));

Generate 2.1 Cypher statements

The Query and CypherQueryTest classes should generate 2.1 statements instead of 2.0.

I would have contributed but I'm struggling getting my old fork up to date with the 2.1.4 tag.

ExecuteWithParams.parameters(Map) incorrect use of local variable

CypherQuery.ExecuteWithParams.parameters(Map<String, Object> parameters) erroneously does a putAll on the same object it passes as a parameter. Needs to be this.parameters.putAll( parameters). Not sure if this code is even being used since Grammer.parameters has its own implementation. Grammar should probably delegate to ExecuteWithParams instead.

protected class ExecuteWithParams
            implements ExecuteWithParameters
    {
        private final Query query;
        private final Map<String, Object> parameters = new HashMap<String, Object>();
...
        @Override
        public ExecuteWithParameters parameters( Map<String, Object> parameters )
        {
            parameters.putAll( parameters );
            return this;
        }

Snapshot builds to match neo4j releases?

Hi,

Is it possible to get snapshot builds of cypher-dsl made (and published via maven) whenever a new snapshot of neo4j is released? Likewise for milestone and stable releases of neo4j?

Thanks!
Chris

Cannot remove lablels

@jexp

I found I cannot remove labels. I see that remove currently only takes reference expressions:

    UpdateNext remove(ReferenceExpression... expressions);

    UpdateNext remove(Iterable<ReferenceExpression> expressions);

Perhaps the easiest way would be to add two more that take LabelValue s

   UpdateNext remove(LabelValue... labelValues);
    UpdateNext remove(Iterable<LabelValue> labelValues);

This would works since cypher allows you to have multiple remove clauses. It is a little icky though in that LabelValue is not an interface but short of adding a new one just for this I don't see a better way. Do you have an opinion on this?

What is the license of this project?

The licensing of the project is not obvious for me:

As far as I can see, the project only uses Neo4j optionally or with a test scope, so in theory, it could be distributed under the Apache license. I believe the fluent API for building queries would be very useful for a lot of openCypher applications.

Migrate to neo4j-contrib

This project should be in the neo4j-contrib github group. The neo4j group should only contain repositories that are part of (or support) the official Neo4j distribution.

License: GPL code inside an Apache 2 project?

Much of the code in the src directory is licensed by Neo technologies under the GPL license on the *.java files. The only clue otherwise is the file asl-2-header.txt which says this code is licensed to Neo under the Apache 2 license. How can I be certain about the license?

Handling for Regexp /../ and Handling of false if Missing Notation

I upgraded to 1.8 from 1.7 yesterday and noticed the following new rules made in 1.8.RC1 have not been incorporated in the latest Cypher DSL 1.8

  1. Removed the /../ literal for regular expressions. Now a normal string literal is used instead
    The dsl is still using /../ notation
  2. Added error when using != instead of <>
    With a query that uses the false if missing notation will receive this error

Incompatibility with recent Querydsl versions

The Querydsl version referred to (2.2.3) is almost a year old. Recent ones (2.7.3) have breaking changes that cause errors when executing. I've stumbled over Ops.EQ_PRIMITIVE and Ops.EQ_OBJECT being unified to Ops.EQ only.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.