text:
# SPARQL Client for RDF.rb

This is a [Ruby][] implementation of a [SPARQL][] client for [RDF.rb][].

* <https://ruby-rdf.github.io/sparql-client/>

[![Gem Version](https://badge.fury.io/rb/sparql-client.svg)](https://badge.fury.io/rb/sparql-client)
[![Build Status](https://github.com/ruby-rdf/sparql-client/workflows/CI/badge.svg?branch=develop)](https://github.com/ruby-rdf/sparql-client/actions?query=workflow%3ACI)
[![Coverage Status](https://coveralls.io/repos/ruby-rdf/sparql-client/badge.svg?branch=master&service=github)](https://coveralls.io/github/ruby-rdf/sparql-client?branch=master)
[![Gitter chat](https://badges.gitter.im/ruby-rdf/rdf.png)](https://gitter.im/ruby-rdf/rdf)

## Features

* Executes queries against any SPARQL 1.0/1.1-compatible endpoint over HTTP,
  or against an `RDF::Queryable` instance, using the `SPARQL` gem.
* Provides a query builder [DSL][] for `ASK`, `SELECT`, `DESCRIBE` and
  `CONSTRUCT` queries.
* Includes preliminary support for some SPARQL 1.1 Update operations.
* Supports tuple result sets in both XML, JSON, CSV and TSV formats, with JSON being
  the preferred default for content-negotiation purposes.
* Supports graph results in any RDF serialization format understood by RDF.rb.
* Returns results using the RDF.rb object model.
* Supports accessing endpoints as read/write [`RDF::Repository`][RDF::Repository]
  instances {SPARQL::Client::Repository}.

## Examples

### Querying a remote SPARQL endpoint

```ruby
require 'sparql/client'
sparql = SPARQL::Client.new("http://dbpedia.org/sparql")
```

### Querying a remote SPARQL endpoint with a custom User-Agent
By default, SPARQL::Client adds a `User-Agent` field to requests, but applications may choose to provide their own, using the `headers` option:

```ruby
require 'sparql/client'
sparql = SPARQL::Client.new("http://dbpedia.org/sparql", headers: {'User-Agent' => 'MyBotName'})
```

### Querying a remote SPARQL endpoint with a specified default graph

```ruby
require 'sparql/client'
sparql = SPARQL::Client.new("http://dbpedia.org/sparql", graph: "http://dbpedia.org")
```

### Querying a `RDF::Repository` instance

```ruby
require 'rdf/trig'
repository = RDF::Repository.load("http://example/dataset.trig")
sparql = SPARQL::Client.new(repository)
```

### Executing a boolean query and outputting the result

```ruby
# ASK WHERE { ?s ?p ?o }
result = sparql.ask.whether([:s, :p, :o]).true?
puts result.inspect   #=> true or false
```

### Executing a tuple query and iterating over the returned solutions

```ruby
# SELECT * WHERE { ?s ?p ?o } OFFSET 100 LIMIT 10
query = sparql.select.where([:s, :p, :o]).offset(100).limit(10)

query.each_solution do |solution|
  puts solution.inspect
end
```

### Executing a graph query and iterating over the returned statements

```ruby
# CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o } LIMIT 10
query = sparql.construct([:s, :p, :o]).where([:s, :p, :o]).limit(10)

query.each_statement do |statement|
  puts statement.inspect
end
```

### Executing an arbitrary textual SPARQL query string

```ruby
result = sparql.query("ASK WHERE { ?s ?p ?o }")

puts result.inspect   #=> true or false
```

### Inserting data into a graph

```ruby
# INSERT DATA { <http://example.org/jhacker> <http://xmlns.com/foaf/0.1/name> "J. Random Hacker" .}
data = RDF::Graph.new do |graph|
  graph << [RDF::URI('http://example.org/jhacker'), RDF::Vocab::FOAF.name, "J. Random Hacker"]
end
sparql.insert_data(data)
```

### Deleting data from a graph

```ruby
# DELETE DATA { <http://example.org/jhacker> <http://xmlns.com/foaf/0.1/name> "J. Random Hacker" .}
data = RDF::Graph.new do |graph|
  graph << [RDF::URI('http://example.org/jhacker'), RDF::Vocab::FOAF.name, "J. Random Hacker"]
end
sparql.delete_data(data)
```

## Documentation

* [SPARQL::Client](https://ruby-rdf.github.io/sparql-client/SPARQL/Client)
  * [SPARQL::Client::Query](https://ruby-rdf.github.io/sparql-client/SPARQL/Client/Query)
  * [SPARQL::Client::Repository](https://ruby-rdf.github.io/sparql-client/SPARQL/Client/Repository)
  * [SPARQL::Client::Update](https://ruby-rdf.github.io/sparql-client/SPARQL/Client/Update)

## Dependencies

* [Ruby](https://ruby-lang.org/) (>= 2.6)
* [RDF.rb](https://rubygems.org/gems/rdf) (~> 3.2)
* [Net::HTTP::Persistent](https://rubygems.org/gems/net-http-persistent) (~> 4.0, >= 4.0.1)
* Soft dependency on [SPARQL](https://rubygems.org/gems/sparql) (~> 3.2)
* Soft dependency on [Nokogiri](https://rubygems.org/gems/nokogiri) (>= 1.12)

## Installation

The recommended installation method is via [RubyGems](https://rubygems.org/).
To install the latest official release of the `SPARQL::Client` gem, do:

    % [sudo] gem install sparql-client

## Download

To get a local working copy of the development repository, do:

    % git clone git://github.com/ruby-rdf/sparql-client.git

Alternatively, download the latest development version as a tarball as
follows:

    % wget https://github.com/ruby-rdf/sparql-client/tarball/master

## Mailing List

* <https://lists.w3.org/Archives/Public/public-rdf-ruby/>

## Authors

* [Arto Bendiken](https://github.com/artob) - <https://ar.to/>
* [Ben Lavender](https://github.com/bhuga) - <https://bhuga.net/>
* [Gregg Kellogg](https://github.com/gkellogg) - <https://greggkellogg.net/>

## Contributors

* [Christoph Badura](https://github.com/bad) - <https://github.com/bad>
* [James Hetherington](https://github.com/jamespjh) - <https://twitter.com/jamespjh>
* [Gabriel Horner](https://github.com/cldwalker) - <https://tagaholic.me/>
* [Nicholas Humfrey](https://github.com/njh) - <https://www.aelius.com/njh/>
* [Fumihiro Kato](https://github.com/fumi) - <https://fumi.me/>
* [David Nielsen](https://github.com/drankard) - <https://github.com/drankard>
* [Thamaraiselvan Poomalai](https://github.com/selvan) - <https://softonaut.blogspot.com/>
* [Michael Sokol](https://github.com/mikaa123) - <https://sokolmichael.com/>
* [Yves Raimond](https://github.com/moustaki) - <https://moustaki.org/>
* [Thomas Feron](https://github.com/thoferon) - <https://github.com/thoferon>
* [Nick Gottlieb](https://github.com/ngottlieb) - <https://www.nicholasgottlieb.com>

## Contributing
This repository uses [Git Flow](https://github.com/nvie/gitflow) to mange development and release activity. All submissions _must_ be on a feature branch based on the _develop_ branch to ease staging and integration.

* Do your best to adhere to the existing coding conventions and idioms.
* Don't use hard tabs, and don't leave trailing whitespace on any line.
* Do document every method you add using [YARD][] annotations. Read the
  [tutorial][YARD-GS] or just look at the existing code for examples.
* Don't touch the `.gemspec`, `VERSION` or `AUTHORS` files. If you need to
  change them, do so on your private branch only.
* Do feel free to add yourself to the `CREDITS` file and the corresponding
  list in the the `README`. Alphabetical order applies.
* Do note that in order for us to merge any non-trivial changes (as a rule
  of thumb, additions larger than about 15 lines of code), we need an
  explicit [public domain dedication][PDD] on record from you,
  which you will be asked to agree to on the first commit to a repo within the organization.
  Note that the agreement applies to all repos in the [Ruby RDF](https://github.com/ruby-rdf/) organization.

## Resources

* <https://ruby-rdf.github.io/sparql-client/>
* <https://github.com/ruby-rdf/sparql-client>
* <https://rubygems.org/gems/sparql-client>
* <https://raa.ruby-lang.org/project/sparql-client/>
* <https://www.ohloh.net/p/rdf>

## License

This is free and unencumbered public domain software. For more information,
see <https://unlicense.org/> or the accompanying {file:UNLICENSE} file.

[Ruby]:            https://ruby-lang.org/
[RDF]:             https://www.w3.org/RDF/
[SPARQL]:          https://en.wikipedia.org/wiki/SPARQL
[SPARQL JSON]:     https://www.w3.org/TR/rdf-sparql-json-res/
[RDF.rb]:          https://rubygems.org/gems/rdf
[RDF::Repository]: https://ruby-rdf.github.io/rdf/RDF/Repository
[DSL]:             https://en.wikipedia.org/wiki/Domain-specific_language
                   "domain-specific language"
[YARD]:            https://yardoc.org/
[YARD-GS]:         https://rubydoc.info/docs/yard/file/docs/GettingStarted.md
[PDD]:             https://unlicense.org/#unlicensing-contributions
[Backports]:       https://rubygems.org/gems/backports


text:
name: Build & deploy documentation
on:
  push:
    branches:
      - master
  workflow_dispatch:
jobs:
  build:
    runs-on: ubuntu-latest
    name: Update gh-pages with docs
    steps:
      - name: Clone repository
        uses: actions/checkout@v3
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.1"
      - name: Install required gem dependencies
        run: gem install yard --no-document
      - name: Build YARD Ruby Documentation
        run: yardoc
      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./doc/yard
          publish_branch: gh-pages

repo_name: ruby-rdf/sparql-client

repo_language: Ruby

mime_type: text/plain


text:
# This workflow runs continuous CI across different versions of ruby on all branches and pull requests to develop.

name: CI
on:
  push:
    branches: [ '**' ]
  pull_request:
    branches: [ develop ]
  workflow_dispatch:

jobs:
  tests:
    name: Ruby ${{ matrix.ruby }} ${{ matrix.gemfile }}
    if: "contains(github.event.commits[0].message, '[ci skip]') == false"
    runs-on: ubuntu-latest
    env:
      CI: true
      BUNDLE_GEMFILE: "${{ matrix.gemfile }}"
      ALLOW_FAILURES: ${{ endsWith(matrix.ruby, 'head') }}
    strategy:
      fail-fast: false
      matrix:
        ruby: [2.6, 2.7, '3.0', 3.1, 3.2, ruby-head, jruby]
        gemfile:
          - Gemfile
          - Gemfile-pure
    steps:
      - name: Clone repository
        uses: actions/checkout@v3
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby }}
      - name: Install dependencies
        run: bundle install --jobs 4 --retry 3
      - name: Run tests
        run: ruby --version; bundle exec rspec spec || $ALLOW_FAILURES
      - name: Coveralls GitHub Action
        uses: coverallsapp/github-action@v2
        if: ${{ matrix.ruby == '3.0' && matrix.gemfile == 'Gemfile' }}
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}


text:
require 'net/http/persistent' # @see https://rubygems.org/gems/net-http-persistent
require 'rdf'                 # @see https://rubygems.org/gems/rdf
require 'rdf/ntriples'        # @see https://rubygems.org/gems/rdf
begin
  require 'nokogiri'
rescue LoadError
  require 'rexml/document'
end

module SPARQL
  ##
  # A SPARQL 1.0/1.1 client for RDF.rb.
  #
  # @see https://www.w3.org/TR/sparql11-query/
  # @see https://www.w3.org/TR/sparql11-protocol/
  # @see https://www.w3.org/TR/sparql11-results-json/
  # @see https://www.w3.org/TR/sparql11-results-csv-tsv/
  class Client
    autoload :Query,      'sparql/client/query'
    autoload :Repository, 'sparql/client/repository'
    autoload :Update,     'sparql/client/update'
    autoload :VERSION,    'sparql/client/version'

    class ClientError < StandardError; end
    class MalformedQuery < ClientError; end
    class ServerError < StandardError; end

    RESULT_JSON = 'application/sparql-results+json'.freeze
    RESULT_XML  = 'application/sparql-results+xml'.freeze
    RESULT_CSV  = 'text/csv'.freeze
    RESULT_TSV  = 'text/tab-separated-values'.freeze
    RESULT_BOOL = 'text/boolean'.freeze                           # Sesame-specific
    RESULT_BRTR = 'application/x-binary-rdf-results-table'.freeze # Sesame-specific
    RESULT_ALL  = [
      RESULT_JSON,
      RESULT_XML,
      RESULT_BOOL,
      "#{RESULT_TSV};q=0.8",
      "#{RESULT_CSV};q=0.2",
      '*/*;q=0.1'
    ].join(', ').freeze
    GRAPH_ALL  = (
      RDF::Format.content_types.keys +
      ['*/*;q=0.1']
    ).join(', ').freeze

    ACCEPT_JSON    = {'Accept' => RESULT_JSON}.freeze
    ACCEPT_XML     = {'Accept' => RESULT_XML}.freeze
    ACCEPT_CSV     = {'Accept' => RESULT_CSV}.freeze
    ACCEPT_TSV     = {'Accept' => RESULT_TSV}.freeze
    ACCEPT_BRTR    = {'Accept' => RESULT_BRTR}.freeze
    ACCEPT_RESULTS = {'Accept' => RESULT_ALL}.freeze
    ACCEPT_GRAPH   = {'Accept' => GRAPH_ALL}.freeze

    DEFAULT_PROTOCOL = 1.0
    DEFAULT_METHOD   = :post

    XMLNS = {'sparql' => 'http://www.w3.org/2005/sparql-results#'}.freeze

    ##
    # The SPARQL endpoint URL, or an RDF::Queryable instance, to use the native SPARQL engine.
    #
    # @return [RDF::URI, RDF::Queryable]
    attr_reader :url

    ##
    # The HTTP headers that will be sent in requests to the endpoint.
    #
    # @return [Hash{String => String}]
    attr_reader :headers

    ##
    # Any miscellaneous configuration.
    #
    # @return [Hash{Symbol => Object}]
    attr_reader :options

    ##
    # Initialize a new sparql client, either using the URL of
    # a SPARQL endpoint or an `RDF::Queryable` instance to use
    # the native SPARQL gem.
    #
    # @param  [String, RDF::Queryable, #to_s]          url
    #   URL of endpoint, or queryable object.
    # @param  [Hash{Symbol => Object}] options
    # @option options [Symbol] :method (DEFAULT_METHOD)
    # @option options [Number] :protocol (DEFAULT_PROTOCOL)
    # @option options [Hash] :headers
    #   HTTP Request headers
    #
    #   Defaults `Accept` header based on available reader content types if triples are expected and to SPARQL result types otherwise, to allow for content negotiation based on available readers.
    #
    #   Defaults  `User-Agent` header, unless one is specified.
    # @option options [Hash] :read_timeout
    def initialize(url, **options, &block)
      case url
      when RDF::Queryable
        @url, @options = url, options.dup
      else
        @url, @options = RDF::URI.new(url.to_s), options.dup
        @headers = @options.delete(:headers) || {}
        @http = http_klass(@url.scheme)

        # Close the http connection when object is deallocated
        ObjectSpace.define_finalizer(self, self.class.finalize(@http))
      end

      if block_given?
        case block.arity
          when 1 then block.call(self)
          else instance_eval(&block)
        end
      end
    end

    # Close the http connection when object is deallocated
    def self.finalize(klass)
      proc do
        if klass.respond_to?(:shutdown)
          begin
            # Attempt asynchronous shutdown
            Thread.new {klass.shutdown}
          rescue ThreadError
            klass.shutdown
          end
        end
      end
    end

    ##
    # Closes a client instance by finishing the connection.
    # The client is unavailable for any further data operations; an IOError is raised if such an attempt is made. I/O streams are automatically closed when they are claimed by the garbage collector.
    # @return [void] `self`
    def close
      @http.shutdown if @http
      @http = nil
      self
    end

    ##
    # Executes a boolean `ASK` query.
    #
    # @param (see Query.ask)
    # @return [Query]
    def ask(*args, **options)
      call_query_method(:ask, *args, **options)
    end

    ##
    # Executes a tuple `SELECT` query.
    #
    # @param (see Query.select)
    # @return [Query]
    def select(*args, **options)
      call_query_method(:select, *args, **options)
    end

    ##
    # Executes a `DESCRIBE` query.
    #
    # @param (see Query.describe)
    # @return [Query]
    def describe(*args, **options)
      call_query_method(:describe, *args, **options)
    end

    ##
    # Executes a graph `CONSTRUCT` query.
    #
    # @param (see Query.construct)
    # @return [Query]
    def construct(*args, **options)
      call_query_method(:construct, *args, **options)
    end

    ##
    # Executes an `INSERT DATA` operation.
    #
    # This requires that the endpoint support SPARQL 1.1 Update.
    #
    # Note that for inserting non-trivial amounts of data, you probably
    # ought to consider using the RDF store's native bulk-loading facilities
    # or APIs, as `INSERT DATA` operations entail comparably higher
    # parsing overhead.
    #
    # @example Inserting data constructed ad-hoc
    #   client.insert_data(RDF::Graph.new { |graph|
    #     graph << [:jhacker, RDF::Vocab::FOAF.name, "J. Random Hacker"]
    #   })
    #
    # @example Inserting data sourced from a file or URL
    #   data = RDF::Graph.load("https://raw.githubusercontent.com/ruby-rdf/rdf/develop/etc/doap.nt")
    #   client.insert_data(data)
    #
    # @example Inserting data into a named graph
    #   client.insert_data(data, graph: "http://example.org/")
    #
    # @param  [RDF::Enumerable] data
    # @param  [Hash{Symbol => Object}] options
    # @option options [RDF::URI, String] :graph
    # @return [void] `self`
    # @see    https://www.w3.org/TR/sparql11-update/#insertData
    def insert_data(data, **options)
      self.update(Update::InsertData.new(data, **options))
    end

    ##
    # Executes a `DELETE DATA` operation.
    #
    # This requires that the endpoint support SPARQL 1.1 Update.
    #
    # @example Deleting data sourced from a file or URL
    #   data = RDF::Graph.load("https://raw.githubusercontent.com/ruby-rdf/rdf/develop/etc/doap.nt")
    #   client.delete_data(data)
    #
    # @example Deleting data from a named graph
    #   client.delete_data(data, graph: "http://example.org/")
    #
    # @param  [RDF::Enumerable] data
    # @param  [Hash{Symbol => Object}] options
    # @option options [RDF::URI, String] :graph
    # @return [void] `self`
    # @see    https://www.w3.org/TR/sparql11-update/#deleteData
    def delete_data(data, **options)
      self.update(Update::DeleteData.new(data, **options))
    end

    ##
    # Executes a `DELETE/INSERT` operation.
    #
    # This requires that the endpoint support SPARQL 1.1 Update.
    #
    # @param  [RDF::Enumerable] delete_graph
    # @param  [RDF::Enumerable] insert_graph
    # @param  [RDF::Enumerable] where_graph
    # @param  [Hash{Symbol => Object}] options
    # @option options [RDF::URI, String] :graph
    # @return [void] `self`
    # @see    https://www.w3.org/TR/sparql11-update/#deleteInsert
    def delete_insert(delete_graph, insert_graph = nil, where_graph = nil, **options)
      self.update(Update::DeleteInsert.new(delete_graph, insert_graph, where_graph, **options))
    end

    ##
    # Executes a `CLEAR GRAPH` operation.
    #
    # This is a convenience wrapper for the {#clear} method.
    #
    # @example `CLEAR GRAPH <http://example.org/>`
    #   client.clear_graph("http://example.org/")
    #
    # @param  [RDF::URI, String] graph_uri
    # @param  [Hash{Symbol => Object}] options
    # @option options [Boolean] :silent
    # @return [void] `self`
    # @see    https://www.w3.org/TR/sparql11-update/#clear
    def clear_graph(graph_uri, **options)
      self.clear(:graph, graph_uri, **options)
    end

    ##
    # Executes a `CLEAR` operation.
    #
    # This requires that the endpoint support SPARQL 1.1 Update.
    #
    # @example `CLEAR GRAPH <http://example.org/>`
    #   client.clear(:graph, RDF::URI("http://example.org/"))
    #
    # @example `CLEAR DEFAULT`
    #   client.clear(:default)
    #
    # @example `CLEAR NAMED`
    #   client.clear(:named)
    #
    # @example `CLEAR ALL`
    #   client.clear(:all)
    #
    # @overload clear(what, *arguments)
    #   @param  [Symbol, #to_sym] what
    #   @param  [Array] arguments splat of other arguments to {Update::Clear}.
    #   @option options [Boolean] :silent
    #   @return [void] `self`
    #
    # @overload clear(what, *arguments, **options)
    #   @param  [Symbol, #to_sym] what
    #   @param  [Array] arguments splat of other arguments to {Update::Clear}.
    #   @param  [Hash{Symbol => Object}] options
    #   @option options [Boolean] :silent
    #   @return [void] `self`
    #
    # @see    https://www.w3.org/TR/sparql11-update/#clear
    def clear(what, *arguments)
      self.update(Update::Clear.new(what, *arguments))
    end

    ##
    # @private
    def call_query_method(meth, *args, **options)
      client = self
      result = Query.send(meth, *args, **options)
      (class << result; self; end).send(:define_method, :execute) do
        client.query(self)
      end
      result
    end

    ##
    # Returns a mapping of blank node results for this client.
    #
    # @private
    def nodes
      @nodes ||= {}
    end

    ##
    # Executes a SPARQL query and returns the parsed results.
    #
    # @param  [String, #to_s]          query
    # @param  [Hash{Symbol => Object}] options
    # @option options [String] :content_type
    # @option options [Hash] :headers
    # @return [Array<RDF::Query::Solution>]
    # @raise [IOError] if connection is closed
    # @see    https://www.w3.org/TR/sparql11-protocol/#query-operation
    def query(query, **options)
      @op = :query
      @alt_endpoint = options[:endpoint]
      case @url
      when RDF::Queryable
        require 'sparql' unless defined?(::SPARQL::Grammar)
        begin
          SPARQL.execute(query, @url, optimize: true, **options)
        rescue SPARQL::MalformedQuery
          $stderr.puts "error running #{query}: #{$!}"
          raise
        end
      else
        parse_response(response(query, **options), **options)
      end
    end

    ##
    # Executes a SPARQL update operation.
    #
    # @param  [String, #to_s]          query
    # @param  [Hash{Symbol => Object}] options
    # @option options [String] :endpoint
    # @option options [String] :content_type
    # @option options [Hash] :headers
    # @return [void] `self`
    # @raise [IOError] if connection is closed
    # @see    https://www.w3.org/TR/sparql11-protocol/#update-operation
    def update(query, **options)
      @op = :update
      @alt_endpoint = options[:endpoint]
      case @url
      when RDF::Queryable
        require 'sparql' unless defined?(::SPARQL::Grammar)
        SPARQL.execute(query, @url, update: true, optimize: true, **options)
      else
        response(query, **options)
      end
      self
    end

    ##
    # Executes a SPARQL query and returns the Net::HTTP::Response of the
    # result.
    #
    # @param [String, #to_s]   query
    # @param  [Hash{Symbol => Object}] options
    # @option options [String] :content_type
    # @option options [Hash] :headers
    # @return [String]
    # @raise [IOError] if connection is closed
    def response(query, **options)
      headers = options[:headers] || @headers
      headers['Accept'] = options[:content_type] if options[:content_type]
      request(query, headers) do |response|
        case response
          when Net::HTTPBadRequest  # 400 Bad Request
            raise MalformedQuery.new(response.body + " Processing query #{query}")
          when Net::HTTPClientError # 4xx
            raise ClientError.new(response.body + " Processing query #{query}")
          when Net::HTTPServerError # 5xx
            raise ServerError.new(response.body + " Processing query #{query}")
          when Net::HTTPSuccess     # 2xx
            response
        end
      end
    end

    ##
    # @param  [Net::HTTPSuccess] response
    # @param  [Hash{Symbol => Object}] options
    # @return [Object]
    def parse_response(response, **options)
      case options[:content_type] || response.content_type
        when NilClass
          response.body
        when RESULT_BOOL # Sesame-specific
          response.body == 'true'
        when RESULT_JSON
          self.class.parse_json_bindings(response.body, nodes)
        when RESULT_XML
          self.class.parse_xml_bindings(response.body, nodes)
        when RESULT_CSV
          self.class.parse_csv_bindings(response.body, nodes)
        when RESULT_TSV
          self.class.parse_tsv_bindings(response.body, nodes)
        else
          parse_rdf_serialization(response, **options)
      end
    end

    ##
    # @param  [String, Hash] json
    # @return [<RDF::Query::Solutions>]
    # @see    https://www.w3.org/TR/rdf-sparql-json-res/#results
    def self.parse_json_bindings(json, nodes = {})
      require 'json' unless defined?(::JSON)
      json = JSON.parse(json.to_s) unless json.is_a?(Hash)
      case
        when json.has_key?('boolean')
          json['boolean']
        when json.has_key?('results')
          solutions = json['results']['bindings'].map do |row|
            row = row.inject({}) do |cols, (name, value)|
              cols.merge(name.to_sym => parse_json_value(value, nodes))
            end
            RDF::Query::Solution.new(row)
          end
          solns = RDF::Query::Solutions.new(solutions)

          # Set variable names explicitly
          if json.fetch('head', {}).has_key?('vars')
            solns.variable_names = json['head']['vars'].map(&:to_sym)
          end
          solns
      end
    end

    ##
    # @param  [Hash{String => String}] value
    # @return [RDF::Value]
    # @see    https://www.w3.org/TR/sparql11-results-json/#select-encode-terms
    # @see    https://www.w3.org/TR/rdf-sparql-json-res/#variable-binding-results
    def self.parse_json_value(value, nodes = {})
      case value['type'].to_sym
        when :bnode
          nodes[id = value['value']] ||= RDF::Node.new(id)
        when :uri
          RDF::URI.new(value['value'])
        when :literal
          RDF::Literal.new(value['value'], datatype: value['datatype'], language: value['xml:lang'])
        when :'typed-literal'
          RDF::Literal.new(value['value'], datatype: value['datatype'])
        when :triple
          s = parse_json_value(value['value']['subject'], nodes)
          p = parse_json_value(value['value']['predicate'], nodes)
          o = parse_json_value(value['value']['object'], nodes)
          RDF::Statement(s, p, o)
        else nil
      end
    end

    ##
    # @param  [String, Array<Array<String>>] csv
    # @return [<RDF::Query::Solutions>]
    # @see    https://www.w3.org/TR/sparql11-results-csv-tsv/
    def self.parse_csv_bindings(csv, nodes = {})
      require 'csv' unless defined?(::CSV)
      csv = CSV.parse(csv.to_s) unless csv.is_a?(Array)
      vars = csv.shift
      solutions = RDF::Query::Solutions.new
      csv.each do |row|
        solution = RDF::Query::Solution.new
        row.each_with_index do |v, i|
          term = case v
          when /^_:(.*)$/ then nodes[$1] ||= RDF::Node($1)
          when /^\w+:.*$/ then RDF::URI(v)
          else RDF::Literal(v)
          end
          solution[vars[i].to_sym] = term
        end
        solutions << solution
      end
      solutions
    end

    ##
    # @param  [String, Array<Array<String>>] tsv
    # @return [<RDF::Query::Solutions>]
    # @see    https://www.w3.org/TR/sparql11-results-csv-tsv/
    def self.parse_tsv_bindings(tsv, nodes = {})
      tsv = tsv.lines.map {|l| l.chomp.split("\t")} unless tsv.is_a?(Array)
      vars = tsv.shift.map {|h| h.sub(/^\?/, '')}
      solutions = RDF::Query::Solutions.new
      tsv.each do |row|
        # Flesh out columns which may be missing
        vars.each_with_index do |_, i|
          row[i] ||= ""
        end
        solution = RDF::Query::Solution.new
        row.each_with_index do |v, i|
          term = case v
          when ""                           then RDF::Literal("")
          when /^\d+\.\d*[eE][+-]?[0-9]+$/  then RDF::Literal::Double.new(v)
          when /^\d*\.\d+[eE][+-]?[0-9]+$/  then RDF::Literal::Double.new(v)
          when /^\d*\.\d+$/                 then RDF::Literal::Decimal.new(v)
          when /^\d+$/                      then RDF::Literal::Integer.new(v)
          else
            RDF::NTriples.unserialize(v) || RDF::Literal(v)
          end
          nodes[term.id] = term if term.is_a? RDF::Node
          solution[vars[i].to_sym] = term
        end
        solutions << solution
      end
      solutions
    end

    ##
    # @param  [String, IO, Nokogiri::XML::Node, REXML::Element] xml
    # @param  [Symbol] library (:nokogiri)
    #   One of :nokogiri or :rexml.
    # @return [<RDF::Query::Solutions>]
    # @see    https://www.w3.org/TR/rdf-sparql-json-res/#results
    def self.parse_xml_bindings(xml, nodes = {}, library: :nokogiri)
      xml.force_encoding(::Encoding::UTF_8) if xml.respond_to?(:force_encoding)

      if defined?(::Nokogiri) && library == :nokogiri
        xml = Nokogiri::XML(xml).root unless xml.is_a?(Nokogiri::XML::Document)
        case
        when boolean = xml.xpath("//sparql:boolean", XMLNS)[0]
          boolean.text == 'true'
        when results = xml.xpath("//sparql:results", XMLNS)[0]
          solutions = results.elements.map do |result|
            row = {}
            result.elements.each do |binding|
              name  = binding.attr('name').to_sym
              value = binding.elements.first
              row[name] = parse_xml_value(value, nodes) if value
            end
            RDF::Query::Solution.new(row)
          end
          solns = RDF::Query::Solutions.new(solutions)

          # Set variable names explicitly
          var_names = xml.xpath("//sparql:head/sparql:variable/@name", XMLNS)
          solns.variable_names = var_names.map(&:to_s)
          solns
        end
      else
        # REXML
        xml = REXML::Document.new(xml).root unless xml.is_a?(REXML::Element)
        case
        when boolean = xml.elements['boolean']
          boolean.text == 'true'
        when results = xml.elements['results']
          solutions = results.elements.map do |result|
            row = {}
            result.elements.each do |binding|
              name  = binding.attributes['name'].to_sym
              value = binding.select { |node| node.kind_of?(::REXML::Element) }.first
              row[name] = parse_xml_value(value, nodes) if value
            end
            RDF::Query::Solution.new(row)
          end
          solns = RDF::Query::Solutions.new(solutions)

          # Set variable names explicitly
          var_names = xml.elements['head'].elements.map {|e| e.attributes['name']}
          solns.variable_names = var_names.map(&:to_sym)
          solns
        end
      end
    end

    ##
    # @param  [Nokogiri::XML::Element, REXML::Element] value
    # @return [RDF::Value]
    # @see    https://www.w3.org/TR/rdf-sparql-json-res/#variable-binding-results
    def self.parse_xml_value(value, nodes = {})
      case value.name.to_sym
        when :bnode
          nodes[id = value.text] ||= RDF::Node.new(id)
        when :uri
          RDF::URI.new(value.text)
        when :literal
          lang     = value.respond_to?(:attr) ? value.attr('xml:lang') : value.attributes['xml:lang']
          datatype = value.respond_to?(:attr) ? value.attr('datatype') : value.attributes['datatype']
          RDF::Literal.new(value.text, language: lang, datatype: datatype)
        when :triple
          # Note, this is order dependent
          res = value.elements.map {|e| e.elements.to_a}.
            flatten.map {|e| parse_xml_value(e, nodes)}
          RDF::Statement(*res)
        else nil
      end
    end

    ##
    # @param  [Net::HTTPSuccess] response
    # @param  [Hash{Symbol => Object}] options
    # @return [RDF::Enumerable]
    def parse_rdf_serialization(response, **options)
      options = {content_type: response.content_type} unless options[:content_type]
      if reader = RDF::Reader.for(**options)
        reader.new(response.body)
      else
        raise RDF::ReaderError, "no RDF reader was found for #{options}."
      end
    end

    ##
    # Serializes a URI or URI string into SPARQL syntax.
    #
    # @param  [RDF::URI, String] uri
    # @return [String]
    # @private
    def self.serialize_uri(uri)
      case uri
        when String then RDF::NTriples.serialize(RDF::URI(uri))
        when RDF::URI then RDF::NTriples.serialize(uri)
        else raise ArgumentError, "expected the graph URI to be a String or RDF::URI, but got #{uri.inspect}"
      end
    end

    ##
    # Serializes an `RDF::Value` into SPARQL syntax.
    #
    # @param  [RDF::Value] value
    # @param  [Boolean] use_vars (false) Use variables in place of BNodes
    # @return [String]
    # @private
    def self.serialize_value(value, use_vars = false)
      # SPARQL queries are UTF-8, but support ASCII-style Unicode escapes, so
      # the N-Triples serializer is fine unless it's a variable:
      case
        when value.nil?      then RDF::Query::Variable.new.to_s
        when value.variable? then value.to_s
        when value.node?     then (use_vars ? RDF::Query::Variable.new(value.id) : value)
        else RDF::NTriples.serialize(value)
      end
    end

    ##
    # Serializes a SPARQL predicate
    #
    # @param [RDF::Value, Array, String] value
    # @param [Fixnum] rdepth
    # @return [String]
    # @private
    def self.serialize_predicate(value,rdepth=0)
      case value
        when nil
          RDF::Query::Variable.new.to_s
        when String then value
        when Array
          s = value.map{|v|serialize_predicate(v,rdepth+1)}.join
          rdepth > 0 ? "(#{s})" : s
        when RDF::Value
          # abbreviate RDF.type in the predicate position per SPARQL grammar
          value.equal?(RDF.type) ? 'a' : serialize_value(value)
      end
    end

    ##
    # Serializes a SPARQL graph
    #
    # @param [RDF::Enumerable] patterns
    # @param  [Boolean] use_vars (false) Use variables in place of BNodes
    # @return [String]
    # @private
    def self.serialize_patterns(patterns, use_vars = false)
      patterns.map do |pattern|
        serialized_pattern = case pattern
        when SPARQL::Client::QueryElement then [pattern.to_s]
        else
          RDF::Statement.from(pattern).to_triple.each_with_index.map do |v, i|
            if i == 1
              SPARQL::Client.serialize_predicate(v)
            else
              SPARQL::Client.serialize_value(v, use_vars)
            end
          end
        end
        serialized_pattern.join(' ') + ' .'
      end
    end

    ##
    # Outputs a developer-friendly representation of this object to `stderr`.
    #
    # @return [void]
    def inspect!
      warn(inspect)
    end

    ##
    # Returns a developer-friendly representation of this object.
    #
    # @return [String]
    def inspect
      sprintf("#<%s:%#0x(%s)>", self.class.name, __id__, url.to_s)
    end

    protected

    ##
    # Returns an HTTP class or HTTP proxy class based on the `http_proxy`
    # and `https_proxy` environment variables.
    #
    # @param  [String] scheme
    # @return [Net::HTTP::Proxy]
    def http_klass(scheme)
      proxy_url = nil
      case scheme
        when 'http'
          value = ENV['http_proxy']
          proxy_url = URI.parse(value) unless value.nil? || value.empty?
        when 'https'
          value = ENV['https_proxy']
          proxy_url = URI.parse(value) unless value.nil? || value.empty?
      end
      klass = Net::HTTP::Persistent.new(name: self.class.to_s, proxy: proxy_url)
      klass.keep_alive =  @options[:keep_alive] || 120
      klass.read_timeout = @options[:read_timeout] || 60
      klass
    end

    ##
    # Performs an HTTP request against the SPARQL endpoint.
    #
    # @param  [String, #to_s]          query
    # @param  [Hash{String => String}] headers
      #   HTTP Request headers
      #
      #   Defaults `Accept` header based on available reader content types if triples are expected and to SPARQL result types otherwise, to allow for content negotiation based on available readers.
      #
      #   Defaults  `User-Agent` header, unless one is specified.
    # @yield  [response]
    # @yieldparam [Net::HTTPResponse] response
    # @return [Net::HTTPResponse]
    # @raise [IOError] if connection is closed
    # @see    https://www.w3.org/TR/sparql11-protocol/#query-operation
    def request(query, headers = {}, &block)
      # Make sure an appropriate Accept header is present
      headers['Accept'] ||= if (query.respond_to?(:expects_statements?) ?
                                query.expects_statements? :
                                (query =~ /CONSTRUCT|DESCRIBE|DELETE|CLEAR/))
        GRAPH_ALL
      else
        RESULT_ALL
      end
      headers['User-Agent'] ||= "Ruby SPARQL::Client/#{SPARQL::Client::VERSION}"

      request = send("make_#{request_method(query)}_request", query, headers)

      request.basic_auth(url.user, url.password) if url.user && !url.user.empty?

      pre_http_hook(request) if respond_to?(:pre_http_hook)

      raise IOError, "Client has been closed" unless @http
      response = @http.request(::URI.parse(url.to_s), request)

      post_http_hook(response) if respond_to?(:post_http_hook)

      10.times do
        if response.kind_of? Net::HTTPRedirection
          response = @http.request(::URI.parse(response['location']), request)
        else
          return block_given? ? block.call(response) : response
        end
      end
      raise ServerError, "Infinite redirect at #{url}. Redirected more than 10 times."
    end

    ##
    # Return the HTTP verb for posting this request.
    # this is useful if you need to override the HTTP verb based on the request being made.
    # (e.g. Marmotta 3.3.0 requires GET for DELETE requests, but can accept POST for INSERT)
    def request_method(query)
      (options[:method] || DEFAULT_METHOD).to_sym
    end

    ##
    # Constructs an HTTP GET request according to the SPARQL Protocol.
    #
    # @param  [String, #to_s]          query
    # @param  [Hash{String => String}] headers
    # @return [Net::HTTPRequest]
    # @see    https://www.w3.org/TR/sparql11-protocol/#query-via-get
    def make_get_request(query, headers = {})
      url = self.url.dup
      url.query_values = (url.query_values || {}).merge(query: query.to_s)
      set_url_default_graph url unless @options[:graph].nil?
      request = Net::HTTP::Get.new(url.request_uri, self.headers.merge(headers))
      request
    end

    ##
    # Constructs an HTTP POST request according to the SPARQL Protocol.
    #
    # @param  [String, #to_s]          query
    # @param  [Hash{String => String}] headers
    # @return [Net::HTTPRequest]
    # @see    https://www.w3.org/TR/sparql11-protocol/#query-via-post-direct
    # @see    https://www.w3.org/TR/sparql11-protocol/#query-via-post-urlencoded
    def make_post_request(query, headers = {})
      if @alt_endpoint.nil?
        url = self.url.dup
        set_url_default_graph url unless @options[:graph].nil?
        endpoint = url.request_uri
      else
        endpoint = @alt_endpoint
      end

      request = Net::HTTP::Post.new(endpoint, self.headers.merge(headers))
      case (self.options[:protocol] || DEFAULT_PROTOCOL).to_s
        when '1.1'
          request['Content-Type'] = 'application/sparql-' + (@op || :query).to_s
          request.body = query.to_s
        when '1.0'
          form_data = {(@op || :query) => query.to_s}
          form_data.merge!(
            {:'default-graph-uri' => @options[:graph]}
          ) if !@options[:graph].nil? && (@op.eql? :query)
          form_data.merge!(
            {:'using-graph-uri' => @options[:graph]}
          ) if !@options[:graph].nil? && (@op.eql? :update)
          request.set_form_data(form_data)
        else
          raise ArgumentError, "unknown SPARQL protocol version: #{self.options[:protocol].inspect}"
      end
      request
    end

    ##
    # Setup url query parameter to use a specified default graph
    #
    # @see    https://www.w3.org/TR/sparql11-protocol/#query-operation
    # @see    https://www.w3.org/TR/sparql11-protocol/#update-operation
    def set_url_default_graph url
      if @options[:graph].is_a? Array
        graphs = @options[:graph].map {|graph|
          CGI::escape(graph)
        }
      else
        graphs = CGI::escape(@options[:graph])
      end
      case @op
      when :query
        url.query_values = (url.query_values || {})
          .merge(:'default-graph-uri' => graphs)
      when :update
        url.query_values = (url.query_values || {})
          .merge(:'using-graph-uri' => graphs)
      end
    end

    # A query element can be used as a component of a query. It may be initialized with a string, which is wrapped in an appropriate container depending on the type of QueryElement. Implements {#to_s} to property serialize when generating a SPARQL query.
    class QueryElement
      attr_reader :elements

      def initialize(*args)
        @elements = args
      end

      def to_s
        raise NotImplemented
      end
    end
  end # Client
end # SPARQL


text:
require 'delegate'

class SPARQL::Client
  ##
  # A SPARQL query builder.
  #
  # @example Iterating over all found solutions
  #   query.each_solution { |solution| puts solution.inspect }
  #
  class Query < RDF::Query
    ##
    # The form of the query.
    #
    # @return [:select, :ask, :construct, :describe]
    # @see    https://www.w3.org/TR/sparql11-query/#QueryForms
    attr_reader :form

    ##
    # @return [Hash{Symbol => Object}]
    attr_reader :options

    ##
    # Creates a boolean `ASK` query.
    #
    # @example ASK WHERE { ?s ?p ?o . }
    #   Query.ask.where([:s, :p, :o])
    #
    # @param  [Hash{Symbol => Object}] options (see {#initialize})
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#ask
    def self.ask(**options)
      self.new(:ask, **options)
    end

    ##
    # Creates a tuple `SELECT` query.
    #
    # @example `SELECT * WHERE { ?s ?p ?o . }`
    #   Query.select.where([:s, :p, :o])
    #
    # @example `SELECT ?s WHERE {?s ?p ?o .}`
    #   Query.select(:s).where([:s, :p, :o])
    #
    # @example `SELECT COUNT(?uri as ?c) WHERE {?uri a owl:Class}`
    #   Query.select(count: {uri: :c}).where([:uri, RDF.type, RDF::OWL.Class])
    #
    # @param  [Array<Symbol>]          variables
    # @return [Query]
    #
    # @overload self.select(*variables, **options)
    #   @param  [Array<Symbol>]          variables
    #   @param  [Hash{Symbol => Object}] options (see {#initialize})
    #   @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#select
    def self.select(*variables, **options)
      self.new(:select, **options).select(*variables)
    end

    ##
    # Creates a `DESCRIBE` query.
    #
    # @example DESCRIBE * WHERE { ?s ?p ?o . }
    #   Query.describe.where([:s, :p, :o])
    #
    # @param  [Array<Symbol, RDF::URI>] variables
    # @return [Query]
    #
    # @overload self.describe(*variables, **options)
    #   @param  [Array<Symbol, RDF::URI>] variables
    #   @param  [Hash{Symbol => Object}] options (see {#initialize})
    #   @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#describe
    def self.describe(*variables, **options)
      self.new(:describe, **options).describe(*variables)
    end

    ##
    # Creates a graph `CONSTRUCT` query.
    #
    # @example CONSTRUCT { ?s ?p ?o . } WHERE { ?s ?p ?o . }
    #   Query.construct([:s, :p, :o]).where([:s, :p, :o])
    #
    # @param  [Array<RDF::Query::Pattern, Array>] patterns
    # @return [Query]
    #
    # @overload self.construct(*variables, **options)
    #   @param  [Array<RDF::Query::Pattern, Array>] patterns
    #   @param  [Hash{Symbol => Object}] options (see {#initialize})
    #   @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#construct
    def self.construct(*patterns, **options)
      self.new(:construct, **options).construct(*patterns) # FIXME
    end

    ##
    # @param  [Symbol, #to_s]          form
    # @overload self.construct(*variables, **options)
    #   @param  [Symbol, #to_s]          form
    #   @param  [Hash{Symbol => Object}] options (see {Client#initialize})
    #   @option options [Hash{Symbol => Symbol}] :count
    #     Contents are symbols relating a variable described within the query,
    #     to the projected variable.
    #
    # @yield  [query]
    # @yieldparam [Query]
    def initialize(form = :ask, **options, &block)
      @subqueries = []
      @form = form.respond_to?(:to_sym) ? form.to_sym : form.to_s.to_sym
      super([], **options, &block)
    end

    ##
    # @example ASK WHERE { ?s ?p ?o . }
    #   Query.ask.where([:s, :p, :o])
    #
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#ask
    def ask
      @form = :ask
      self
    end

    ##
    # @example `SELECT * WHERE { ?s ?p ?o . }`
    #   Query.select.where([:s, :p, :o])
    #
    # @example `SELECT ?s WHERE {?s ?p ?o .}`
    #   Query.select(:s).where([:s, :p, :o])
    #
    # @example `SELECT COUNT(?uri as ?c) WHERE {?uri a owl:Class}`
    #   Query.select(count: {uri: :c}).where([:uri, RDF.type, RDF::OWL.Class])
    #
    # @param  [Array<Symbol>, Hash{Symbol => RDF::Query::Variable}] variables
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#select
    def select(*variables)
      @values = if variables.length == 1 && variables.first.is_a?(Hash)
        variables.to_a
      else
        variables.map { |var| [var, RDF::Query::Variable.new(var)] }
      end
      self
    end

    ##
    # @example DESCRIBE * WHERE { ?s ?p ?o . }
    #   Query.describe.where([:s, :p, :o])
    #
    # @param  [Array<Symbol>] variables
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#describe
    def describe(*variables)
      @values = variables.map { |var|
        [var, var.is_a?(RDF::URI) ? var : RDF::Query::Variable.new(var)]
      }
      self
    end

    ##
    # @example CONSTRUCT { ?s ?p ?o . } WHERE { ?s ?p ?o . }
    #   Query.construct([:s, :p, :o]).where([:s, :p, :o])
    #
    # @param  [Array<RDF::Query::Pattern, Array>] patterns
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#construct
    def construct(*patterns)
      options[:template] = build_patterns(patterns)
      self
    end

    ##
    # @example SELECT * FROM <a> WHERE \{ ?s ?p ?o . \}
    #   Query.select.from(RDF::URI.new(a)).where([:s, :p, :o])
    #
    # @param [RDF::URI] uri
    # @return [Query]
    # @see https://www.w3.org/TR/sparql11-query/#specifyingDataset
    def from(uri)
      options[:from] = uri
      self
    end

    ##
    # @example SELECT * WHERE { ?s ?p ?o . }
    #   Query.select.where([:s, :p, :o])
    #   Query.select.whether([:s, :p, :o])
    #
    # @example SELECT * WHERE { { SELECT * WHERE { ?s ?p ?o . } } . ?s ?p ?o . }
    #   subquery = Query.select.where([:s, :p, :o])
    #   Query.select.where([:s, :p, :o], subquery)
    #
    # @example SELECT * WHERE { { SELECT * WHERE { ?s ?p ?o . } } . ?s ?p ?o . }
    #   Query.select.where([:s, :p, :o]) do |q|
    #     q.select.where([:s, :p, :o])
    #   end
    #
    # Block form can be used for chaining calls in addition to creating sub-select queries.
    #
    # @example SELECT * WHERE { ?s ?p ?o . } ORDER BY ?o
    #   Query.select.where([:s, :p, :o]) do
    #     order(:o)
    #   end
    #
    # @param  [Array<RDF::Query::Pattern, Array>] patterns_queries
    #   splat of zero or more patterns followed by zero or more queries.
    # @yield [query]
    #   Yield form with or without argument; without an argument, evaluates within the query.
    # @yieldparam [SPARQL::Client::Query] query Actually a delegator to query. Methods other than `#select` are evaluated against `self`. For `#select`, a new Query is created, and the result added as a subquery.
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#GraphPattern
    def where(*patterns_queries, &block)
      subqueries, patterns = patterns_queries.partition {|pq| pq.is_a? SPARQL::Client::Query}
      @patterns += build_patterns(patterns)
      @subqueries += subqueries

      if block_given?
        decorated_query = WhereDecorator.new(self)
        case block.arity
          when 1 then block.call(decorated_query)
          else decorated_query.instance_eval(&block)
        end
      end
      self
    end

    alias_method :whether, :where

    # @private
    class WhereDecorator < SimpleDelegator
      def select(*variables)
        query = SPARQL::Client::Query.select(*variables)
        __getobj__.instance_variable_get(:@subqueries) << query
        query
      end
    end

    ##
    # @example SELECT * WHERE { ?s ?p ?o . } ORDER BY ?o
    #   Query.select.where([:s, :p, :o]).order(:o)
    #   Query.select.where([:s, :p, :o]).order_by(:o)
    #
    # @example SELECT * WHERE { ?s ?p ?o . } ORDER BY ?o ?p
    #   Query.select.where([:s, :p, :o]).order_by(:o, :p)
    #
    # @example SELECT * WHERE { ?s ?p ?o . } ORDER BY ASC(?o) DESC(?p)
    #   Query.select.where([:s, :p, :o]).order_by(o: :asc, p: :desc)
    #
    # @param  [Array<Symbol, String>] variables
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#modOrderBy
    def order(*variables)
      options[:order_by] = variables
      self
    end

    alias_method :order_by, :order

    ##
    # @example SELECT * WHERE { ?s ?p ?o . } ORDER BY ASC(?o)
    #   Query.select.where([:s, :p, :o]).order.asc(:o)
    #   Query.select.where([:s, :p, :o]).asc(:o)
    #
    # @param  [Array<Symbol, String>] var
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#modOrderBy
    def asc(var)
      (options[:order_by] ||= []) << {var => :asc}
      self
    end

    ##
    # @example SELECT * WHERE { ?s ?p ?o . } ORDER BY DESC(?o)
    #   Query.select.where([:s, :p, :o]).order.desc(:o)
    #   Query.select.where([:s, :p, :o]).desc(:o)
    #
    # @param  [Array<Symbol, String>] var
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#modOrderBy
    def desc(var)
      (options[:order_by] ||= []) << {var => :desc}
      self
    end

    ##
    # @example SELECT ?s WHERE { ?s ?p ?o . } GROUP BY ?s
    #   Query.select(:s).where([:s, :p, :o]).group_by(:s)
    #
    # @param  [Array<Symbol, String>] variables
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#groupby
    def group(*variables)
      options[:group_by] = variables
      self
    end

    alias_method :group_by, :group

    ##
    # @example SELECT DISTINCT ?s WHERE { ?s ?p ?o . }
    #   Query.select(:s).distinct.where([:s, :p, :o])
    #
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#modDuplicates
    def distinct(state = true)
      options[:distinct] = state
      self
    end

    ##
    # @example SELECT REDUCED ?s WHERE { ?s ?p ?o . }
    #   Query.select(:s).reduced.where([:s, :p, :o])
    #
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#modDuplicates
    def reduced(state = true)
      options[:reduced] = state
      self
    end

    ##
    # @example SELECT * WHERE { GRAPH ?g { ?s ?p ?o . } }
    #   Query.select.graph(:g).where([:s, :p, :o])
    #
    # @param  [RDF::Value] graph_uri_or_var
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#queryDataset
    def graph(graph_uri_or_var)
      options[:graph] = case graph_uri_or_var
        when Symbol then RDF::Query::Variable.new(graph_uri_or_var)
        when String then RDF::URI(graph_uri_or_var)
        when RDF::Value then graph_uri_or_var
        else raise ArgumentError
      end
      self
    end

    ##
    # @example SELECT * WHERE { ?s ?p ?o . } OFFSET 100
    #   Query.select.where([:s, :p, :o]).offset(100)
    #
    # @param  [Integer, #to_i] start
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#modOffset
    def offset(start)
      slice(start, nil)
    end

    ##
    # @example SELECT * WHERE { ?s ?p ?o . } LIMIT 10
    #   Query.select.where([:s, :p, :o]).limit(10)
    #
    # @param  [Integer, #to_i] length
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#modResultLimit
    def limit(length)
      slice(nil, length)
    end

    ##
    # @example SELECT * WHERE { ?s ?p ?o . } OFFSET 100 LIMIT 10
    #   Query.select.where([:s, :p, :o]).slice(100, 10)
    #
    # @param  [Integer, #to_i] start
    # @param  [Integer, #to_i] length
    # @return [Query]
    def slice(start, length)
      options[:offset] = start.to_i if start
      options[:limit] = length.to_i if length
      self
    end

    ##
    # @overload prefix(prefix: uri)
    #   @example PREFIX dc: <http://purl.org/dc/elements/1.1/> PREFIX foaf: <http://xmlns.com/foaf/0.1/> SELECT * WHERE \{ ?s ?p ?o . \}
    #     Query.select.
    #       prefix(dc: RDF::URI("http://purl.org/dc/elements/1.1/")).
    #       prefix(foaf: RDF::URI("http://xmlns.com/foaf/0.1/")).
    #       where([:s, :p, :o])
    #
    #   @param [RDF::URI] uri
    #   @param [Symbol, String] prefix
    #   @return [Query]
    #
    # @overload prefix(string)
    #   @example PREFIX dc: <http://purl.org/dc/elements/1.1/> PREFIX foaf: <http://xmlns.com/foaf/0.1/> SELECT * WHERE \{ ?s ?p ?o . \}
    #     Query.select.
    #       prefix("dc: <http://purl.org/dc/elements/1.1/>").
    #       prefix("foaf: <http://xmlns.com/foaf/0.1/>").
    #       where([:s, :p, :o])
    #
    #   @param [string] string
    #   @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#prefNames
    def prefix(val)
      options[:prefixes] ||= []
      if val.kind_of? String
        options[:prefixes] << val
      elsif val.kind_of? Hash
        val.each do |k, v|
          options[:prefixes] << "#{k}: <#{v}>"
        end
      else
        raise ArgumentError, "prefix must be a kind of String or a Hash"
      end
      self
    end

    ##
    # @example SELECT * WHERE \{ ?s ?p ?o . OPTIONAL \{ ?s a ?o . ?s \<http://purl.org/dc/terms/abstract\> ?o . \} \}
    #   Query.select.where([:s, :p, :o]).
    #     optional([:s, RDF.type, :o], [:s, RDF::Vocab::DC.abstract, :o])
    #
    # The block form can be used for adding filters:
    #
    # @example ASK WHERE { ?s ?p ?o . OPTIONAL { ?s ?p ?o . FILTER(regex(?s, 'Abiline, Texas'))} }
    #   Query.ask.where([:s, :p, :o]).optional([:s, :p, :o]) do
    #     filter("regex(?s, 'Abiline, Texas')")
    #   end
    #
    # @param  [Array<RDF::Query::Pattern, Array>] patterns
    #   splat of zero or more patterns followed by zero or more queries.
    # @yield [query]
    #   Yield form with or without argument; without an argument, evaluates within the query.
    # @yieldparam [SPARQL::Client::Query] query used for creating filters on the optional patterns.
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#optionals
    def optional(*patterns, &block)
      (options[:optionals] ||= []) << build_patterns(patterns)

      if block_given?
        # Steal options[:filters]
        query_filters = options[:filters]
        options[:filters] = []
        case block.arity
          when 1 then block.call(self)
          else instance_eval(&block)
        end
        options[:optionals].last.concat(options[:filters])
        options[:filters] = query_filters
      end

      self
    end

    ##
    # Federated Queries via the SERVICE keyword.
    #
    # Supports limited use of the SERVICE keyword with an endpoint term, a sequence of patterns, a query, or a block.
    #
    # @example SELECT * WHERE \{ ?s ?p1 ?o1 . SERVICE ?l \{ ?s ?p2 ?o2 \} \}
    #   Query.select.where([:s, :p1, :o1]).
    #     service(:l, [:s, :p2, :o2])
    #
    # @example SELECT * WHERE \{ ?book <http://purl.org/dc/terms/title> ?title . SERVICE ?l \{ ?book <http://purl.org/dc/elements/1.1/title> ?title . FILTER(langmatches(?title, 'en')) \} \}
    #   query1 = SPARQL::Client::Query.select.
    #     where([:book, RDF::Vocab::DC11.title, :title]).
    #     filter("langmatches(?title, 'en')")
    #   Query.select.where([:book, RDF::Vocab::DC.title, :title]).service(?l, query1)
    #
    # The block form can be used for more complicated queries, using the `select` form (note, use either block or argument forms, not both):
    #
    # @example SELECT * WHERE \{ ?book dc:title ?title \} SERVICE ?l \{ ?book dc11:title ?title \}
    #   query1 = SPARQL::Client::Query.select.where([:book, RDF::Vocab::DC11.title, :title])
    #   Query.select.where([:book, RDF::Vocab::DC.title, :title]).service :l do |q|
    #     q.select.
    #       where([:book, RDF::Vocab::DC11.title, :title])
    #   end
    #
    # @example SELECT * WHERE \{ ?s ?p1 ?o1 . SERVICE SILENT ?l \{ ?s ?p2 ?o2 \} \}
    #   Query.select.where([:s, :p1, :o1]).
    #     service(:l, [:s, :p2, :o2], silent: true)
    #
    # @param  [Array<RDF::Query::Pattern, Array>] patterns
    #   splat of zero or more patterns followed by zero or more queries.
    # @param [Boolean] silent
    # @yield [query]
    #   Yield form with or without argument; without an argument, evaluates within the query.
    # @yieldparam [SPARQL::Client::Query] query used for adding select clauses.
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-federated-query/
    def service(endpoint, *patterns, silent: false, &block)
      service = {
        endpoint: (endpoint.is_a?(Symbol) ? RDF::Query::Variable.new(endpoint) : endpoint),
        silent: silent,
        query: nil
      }
      (options[:services] ||= []) << service

      if block_given?
        raise ArgumentError, "#service requires either arguments or a block, not both." unless patterns.empty?
        # Evaluate calls in a new query instance
        query = self.class.select.where
        case block.arity
          when 1 then block.call(query)
          else query.instance_eval(&block)
        end
        service[:query] = query
      elsif patterns.all? {|p| p.is_a?(SPARQL::Client::Query)}
        # With argument form, all must be patterns or queries
        raise ArgumentError, "#service arguments are triple patterns or a query, not both." if patterns.length != 1
        service[:query] = patterns.first
      elsif patterns.all? {|p| p.is_a?(Array)}
        # With argument form, all must be patterns, or queries
        service[:query] = self.class.select.where(*patterns)
      else
        raise ArgumentError, "#service arguments are triple patterns a query, not both."
      end

      self
    end

    ##
    # @example SELECT * WHERE \{ ?book dc:title ?title \} UNION \{ ?book dc11:title ?title \}
    #   Query.select.where([:book, RDF::Vocab::DC.title, :title]).
    #     union([:book, RDF::Vocab::DC11.title, :title])
    #
    # @example SELECT * WHERE \{ ?book dc:title ?title \} UNION \{ ?book dc11:title ?title . FILTER(langmatches(lang(?title), 'EN'))\}
    #   query1 = SPARQL::Client::Query.select.
    #     where([:book, RDF::Vocab::DC11.title, :title]).
    #     filter("langmatches(?title, 'en')")
    #   Query.select.where([:book, RDF::Vocab::DC.title, :title]).union(query1)
    #
    # The block form can be used for more complicated queries, using the `select` form (note, use either block or argument forms, not both):
    #
    # @example SELECT * WHERE \{ ?book dc:title ?title \} UNION \{ ?book dc11:title ?title . FILTER(langmatches(lang(?title), 'EN'))\}
    #   query1 = SPARQL::Client::Query.select.where([:book, RDF::Vocab::DC11.title, :title]).filter("langmatches(?title, 'en')")
    #   Query.select.where([:book, RDF::Vocab::DC.title, :title]).union do |q|
    #     q.select.
    #       where([:book, RDF::Vocab::DC11.title, :title]).
    #       filter("langmatches(?title, 'en')")
    #   end
    #
    # @param  [Array<RDF::Query::Pattern, Array>] patterns
    #   splat of zero or more patterns followed by zero or more queries.
    # @yield [query]
    #   Yield form with or without argument; without an argument, evaluates within the query.
    # @yieldparam [SPARQL::Client::Query] query used for adding select clauses.
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#alternatives
    def union(*patterns, &block)
      options[:unions] ||= []

      if block_given?
        raise ArgumentError, "#union requires either arguments or a block, not both." unless patterns.empty?
        # Evaluate calls in a new query instance
        query = self.class.select
        case block.arity
          when 1 then block.call(query)
          else query.instance_eval(&block)
        end
        options[:unions] << query
      elsif patterns.all? {|p| p.is_a?(SPARQL::Client::Query)}
        # With argument form, all must be patterns or queries
        options[:unions] += patterns
      elsif patterns.all? {|p| p.is_a?(Array)}
        # With argument form, all must be patterns, or queries
        options[:unions] << self.class.select.where(*patterns)
      else
        raise ArgumentError, "#union arguments are triple patterns or queries, not both."
      end

      self
    end

    ##
    # @example SELECT * WHERE \{ ?book dc:title ?title . MINUS \{ ?book dc11:title ?title \} \}
    #   Query.select.where([:book, RDF::Vocab::DC.title, :title]).
    #     minus([:book, RDF::Vocab::DC11.title, :title])
    #
    # @example SELECT * WHERE \{ ?book dc:title ?title MINUS \{ ?book dc11:title ?title . FILTER(langmatches(lang(?title), 'EN')) \} \}
    #   query1 = SPARQL::Client::Query.select.
    #     where([:book, RDF::Vocab::DC11.title, :title]).
    #     filter("langmatches(?title, 'en')")
    #   Query.select.where([:book, RDF::Vocab::DC.title, :title]).minus(query1)
    #
    # The block form can be used for more complicated queries, using the `select` form (note, use either block or argument forms, not both):
    #
    # @example SELECT * WHERE \{ ?book dc:title ?title MINUS \{ ?book dc11:title ?title . FILTER(langmatches(lang(?title), 'EN'))\} \}
    #   query1 = SPARQL::Client::Query.select.where([:book, RDF::Vocab::DC11.title, :title]).filter("langmatches(?title, 'en')")
    #   Query.select.where([:book, RDF::Vocab::DC.title, :title]).minus do |q|
    #     q.select.
    #       where([:book, RDF::Vocab::DC11.title, :title]).
    #       filter("langmatches(?title, 'en')")
    #   end
    #
    # @param  [Array<RDF::Query::Pattern, Array>] patterns
    #   splat of zero or more patterns followed by zero or more queries.
    # @yield [query]
    #   Yield form with or without argument; without an argument, evaluates within the query.
    # @yieldparam [SPARQL::Client::Query] query used for adding select clauses.
    # @return [Query]
    # @see    https://www.w3.org/TR/sparql11-query/#negation
    def minus(*patterns, &block)
      options[:minuses] ||= []

      if block_given?
        raise ArgumentError, "#minus requires either arguments or a block, not both." unless patterns.empty?
        # Evaluate calls in a new query instance
        query = self.class.select
        case block.arity
          when 1 then block.call(query)
          else query.instance_eval(&block)
        end
        options[:minuses] << query
      elsif patterns.all? {|p| p.is_a?(SPARQL::Client::Query)}
        # With argument form, all must be patterns or queries
        options[:minuses] += patterns
      elsif patterns.all? {|p| p.is_a?(Array)}
        # With argument form, all must be patterns, or queries
        options[:minuses] << self.class.select.where(*patterns)
      else
        raise ArgumentError, "#minus arguments are triple patterns or queries, not both."
      end

      self
    end

    ##
    # Specify inline data for a query
    #
    # @overload values
    #   Values returned from previous query.
    #
    #   @return [Array<Array(key, RDF::Value)>]
    #
    # @overload values(vars, *data)
    #   @example single variable with multiple values
    #     Query.select
    #      .where([:s, RDF::URI('http://purl.org/dc/terms/title'), :title])
    #      .values(:title, "This title", "Another title")
    #
    #   @example multiple variables with multiple values
    #     Query.select
    #      .where([:s, RDF::URI('http://purl.org/dc/terms/title'), :title],
    #             [:s, RDF.type, :type])
    #      .values([:type, :title],
    #              [RDF::URI('http://pcdm.org/models#Object'), "This title"],
    #              [RDF::URI('http://pcdm.org/models#Collection', 'Another title'])
    #
    #   @example multiple variables with UNDEF
    #     Query.select
    #      .where([:s, RDF::URI('http://purl.org/dc/terms/title'), :title],
    #             [:s, RDF.type, :type])
    #      .values([:type, :title],
    #              [nil "This title"],
    #              [RDF::URI('http://pcdm.org/models#Collection', nil])
    #
    #   @param [Symbol, Array<Symbol>] vars
    #   @param [Array<RDF::Term, String, nil>] *data
    #   @return [Query]
    def values(*args)
      return @values if args.empty?
      vars, *data = *args
      vars = Array(vars).map {|var| RDF::Query::Variable.new(var)}
      if vars.length == 1
        # data may be a in array form or simple form
        if data.any? {|d| d.is_a?(Array)} && !data.all? {|d| d.is_a?(Array)}
          raise ArgumentError, "values data must all be in array form or all simple"
        end
        data = data.map {|d| Array(d)}
      end

      # Each data value must be an array with the same number of entries as vars
      unless data.all? {|d| d.is_a?(Array) && d.all? {|dd| dd.is_a?(RDF::Value) || dd.is_a?(String) || dd.nil?}}
        raise ArgumentError, "values data must each be an array of terms, strings, or nil"
      end

      # Turn strings into Literals
      data = data.map do |d|
        d.map do |nil_literal_or_term|
          case nil_literal_or_term
          when nil then nil
          when String then RDF::Literal(nil_literal_or_term)
          when RDF::Value then nil_literal_or_term
          else raise ArgumentError
          end
        end
      end
      options[:values] = [vars, *data]
      self
    end

    ##
    # @return expects_statements?
    def expects_statements?
      [:construct, :describe].include?(form)
    end

    ##
    # @private
    def build_patterns(patterns)
      patterns.map {|pattern| RDF::Query::Pattern.from(pattern)}
    end

    ##
    # @example ASK WHERE { ?s ?p ?o . FILTER(regex(?s, 'Abiline, Texas')) }
    #   Query.ask.where([:s, :p, :o]).filter("regex(?s, 'Abiline, Texas')")
    # @return [Query]
    def filter(string)
      ((options[:filters] ||= []) << Filter.new(string)) if string and not string.empty?
      self
    end

    ##
    # @return [Boolean]
    def true?
      case result
        when TrueClass, FalseClass then result
        when RDF::Literal::Boolean then result.true?
        when Enumerable then !result.empty?
        else false
      end
    end

    ##
    # @return [Boolean]
    def false?
      !true?
    end

    ##
    # @return [Enumerable<RDF::Query::Solution>]
    def solutions
      result
    end

    ##
    # @yield  [statement]
    # @yieldparam [RDF::Statement]
    # @return [Enumerator]
    def each_statement(&block)
      result.each_statement(&block)
    end

    # Enumerates over each matching query solution.
    #
    # @yield  [solution]
    # @yieldparam [RDF::Query::Solution] solution
    # @return [Enumerator]
    def each_solution(&block)
      @solutions = result
      super
    end

    ##
    # @return [Object]
    def result
      @result ||= execute
    end

    ##
    # @return [Object]
    def execute
      raise NotImplementedError
    end

    ##
    # Returns the string representation of this query.
    #
    # @return [String]
    def to_s
      buffer = [form.to_s.upcase]

      case form
        when :select, :describe
          only_count = values.empty? && options[:count]
          buffer << 'DISTINCT' if options[:distinct] and not only_count
          buffer << 'REDUCED'  if options[:reduced]
          buffer << ((values.empty? and not options[:count]) ? '*' : values.map { |v| SPARQL::Client.serialize_value(v[1]) }.join(' '))
          if options[:count]
            options[:count].each do |var, count|
              buffer << '( COUNT(' + (options[:distinct] ? 'DISTINCT ' : '') +
                (var.is_a?(String) ? var : "?#{var}") + ') AS ' + (count.is_a?(String) ? count : "?#{count}") + ' )'
            end
          end
        when :construct
          buffer << '{'
          buffer += SPARQL::Client.serialize_patterns(options[:template])
          buffer << '}'
      end

      buffer << "FROM #{SPARQL::Client.serialize_value(options[:from])}" if options[:from]

      unless patterns.empty? && form == :describe
        buffer += self.to_s_ggp.unshift('WHERE')
      end

      options.fetch(:unions, []).each do |query|
        buffer += query.to_s_ggp.unshift('UNION')
      end

      if options[:group_by]
        buffer << 'GROUP BY'
        buffer += options[:group_by].map { |var| var.is_a?(String) ? var : "?#{var}" }
      end

      if options[:order_by]
        buffer << 'ORDER BY'
        options[:order_by].map { |elem|
          case elem
            # .order_by({ var1: :asc, var2: :desc})
            when Hash
              elem.each { |key, val|
                # check provided values
                if !key.is_a?(Symbol)
                  raise ArgumentError, 'keys of hash argument must be a Symbol'
                elsif !val.is_a?(Symbol) || (val != :asc && val != :desc)
                  raise ArgumentError, 'values of hash argument must either be `:asc` or `:desc`'
                end
                buffer << "#{val == :asc ? 'ASC' : 'DESC'}(?#{key})"
              }
            # .order_by([:var1, :asc], [:var2, :desc])
            when Array
              # check provided values
              if elem.length != 2
                raise ArgumentError, 'array argument must specify two elements'
              elsif !elem[0].is_a?(Symbol)
                raise ArgumentError, '1st element of array argument must contain a Symbol'
              elsif !elem[1].is_a?(Symbol) || (elem[1] != :asc && elem[1] != :desc)
                raise ArgumentError, '2nd element of array argument must either be `:asc` or `:desc`'
              end
              buffer << "#{elem[1] == :asc ? 'ASC' : 'DESC'}(?#{elem[0]})"
            # .order_by(:var1, :var2)
            when Symbol
              buffer << "?#{elem}"
            # .order_by('ASC(?var1) DESC(?var2)')
            when String
              buffer << elem
            else
              raise ArgumentError, 'argument provided to `order()` must either be an Array, Symbol or String'
          end
        }
      end

      buffer << "OFFSET #{options[:offset]}" if options[:offset]
      buffer << "LIMIT #{options[:limit]}"   if options[:limit]
      options[:prefixes].reverse.each { |e| buffer.unshift("PREFIX #{e}") } if options[:prefixes]

      buffer.join(' ')
    end

    # Serialize a Group Graph Pattern
    # @private
    def to_s_ggp
      buffer = ["{"]

      if options[:graph]
        buffer << 'GRAPH ' + SPARQL::Client.serialize_value(options[:graph])
        buffer << '{'
      end

      @subqueries.each do |sq|
        buffer << "{ #{sq.to_s} } ."
      end

      buffer += SPARQL::Client.serialize_patterns(patterns)
      if options[:optionals]
        options[:optionals].each do |patterns|
          buffer << 'OPTIONAL {'
          buffer += SPARQL::Client.serialize_patterns(patterns)
          buffer << '}'
        end
      end
      if options[:filters]
        buffer += options[:filters].map(&:to_s)
      end

      if options[:services]
        options[:services].each do |service|
          buffer << 'SERVICE'
          buffer << 'SILENT' if service[:silent]
          buffer << SPARQL::Client.serialize_value(service[:endpoint])
          buffer << service[:query].to_s_ggp
        end
      end

      if options[:values]
        vars = options[:values].first.map {|var| SPARQL::Client.serialize_value(var)}
        buffer << "VALUES (#{vars.join(' ')}) {"
        options[:values][1..-1].each do |data_block_value|
          buffer << '('
          buffer << data_block_value.map do |value|
            value.nil? ? 'UNDEF' : SPARQL::Client.serialize_value(value)
          end.join(' ')
          buffer << ')'
        end
        buffer << '}'
      end
      if options[:graph]
        buffer << '}' # GRAPH
      end

      options.fetch(:minuses, []).each do |query|
        buffer += query.to_s_ggp.unshift('MINUS')
      end

      buffer << '}'
      buffer
    end

    ##
    # Outputs a developer-friendly representation of this query to `stderr`.
    #
    # @return [void]
    def inspect!
      warn(inspect)
      self
    end

    ##
    # Returns a developer-friendly representation of this query.
    #
    # @return [String]
    def inspect
      sprintf("#<%s:%#0x(%s)>", self.class.name, __id__, to_s)
    end

    # Allow Filters to be
    class Filter < SPARQL::Client::QueryElement
      def initialize(*args)
        super
      end

      def to_s
        "FILTER(#{elements.join(' ')})"
      end
    end
  end
end


text:
class SPARQL::Client
  ##
  # A read-only repository view of a SPARQL endpoint.
  #
  # @see `RDF::Repository`
  class Repository < RDF::Repository
    # @return [SPARQL::Client]
    attr_reader :client

    ##
    # @param [URI, #to_s]    uri
    #   Endpoint of this repository
    # @param [Hash{Symbol => Object}] options passed to RDF::Repository
    def initialize(uri: nil, **options, &block)
      raise ArgumentError, "uri is a required parameter" unless uri
      @options = options.merge(uri: uri)
      @update_client = SPARQL::Client.new(options.delete(:update_endpoint), **options) if options[:update_endpoint]
      @client  = SPARQL::Client.new(uri, **options)
      super(**@options, &block)
    end

    ##
    # Returns the client for the update_endpoint if specified, otherwise the
    # {#client}.
    #
    # @return [SPARQL::Client]
    def update_client
      @update_client || @client
    end

    # Enumerates each RDF statement in this repository.
    #
    # @yield  [statement]
    # @yieldparam [RDF::Statement] statement
    # @see    RDF::Repository#each
    def each(&block)
      client.construct([:s, :p, :o]).where([:s, :p, :o]).each_statement(&block)
    end

    ##
    # Iterates the given block for each RDF statement.
    #
    # If no block was given, returns an enumerator.
    #
    # The order in which statements are yielded is undefined.
    #
    # @overload each_statement
    #   @yield  [statement]
    #     each statement
    #   @yieldparam  [RDF::Statement] statement
    #   @yieldreturn [void] ignored
    #   @return [void]
    #
    # @overload each_statement
    #   @return [Enumerator<RDF::Statement>]
    def each_statement(&block)
      if block_given?
        # Invoke {#each} in the containing class:
        each(&block)
      end
      enum_statement
    end

    ##
    # @private
    # @see RDF::Enumerable#supports?
    def supports?(feature)
      case feature.to_sym
        # statement contexts / named graphs
        when :context     then false
        when :graph_name  then false
        when :inference   then false  # forward-chaining inference
        when :validity    then false
        when :literal_equality then true
        else false
      end
    end

    ##
    # Returns `true` if this repository contains the given subject.
    #
    # @param  [RDF::Resource] subject
    # @return [Boolean]
    # @see    RDF::Repository#has_subject?
    def has_subject?(subject)
      client.ask.whether([subject, :p, :o]).true?
    end

    ##
    # Returns `true` if this repository contains the given predicate.
    #
    # @param  [RDF::URI] predicate
    # @return [Boolean]
    # @see    RDF::Repository#has_predicate?
    def has_predicate?(predicate)
      client.ask.whether([:s, predicate, :o]).true?
    end

    ##
    # Returns `true` if this repository contains the given object.
    #
    # @param  [RDF::Value] object
    # @return [Boolean]
    # @see    RDF::Repository#has_object?
    def has_object?(object)
      client.ask.whether([:s, :p, object]).true?
    end

    ##
    # Iterates over each subject in this repository.
    #
    # @yield  [subject]
    # @yieldparam [RDF::Resource] subject
    # @return [Enumerator]
    # @see    RDF::Repository#each_subject?
    def each_subject(&block)
      if block_given?
        client.select(:s, distinct: true).where([:s, :p, :o]).each_solution { |solution| block.call(solution[:s]) }
      end
      enum_subject
    end

    ##
    # Iterates over each predicate in this repository.
    #
    # @yield  [predicate]
    # @yieldparam [RDF::URI] predicate
    # @return [Enumerator]
    # @see    RDF::Repository#each_predicate?
    def each_predicate(&block)
      if block_given?
        client.select(:p, distinct: true).where([:s, :p, :o]).each_solution { |solution| block.call(solution[:p]) }
      end
      enum_predicate
    end

    ##
    # Iterates over each object in this repository.
    #
    # @yield  [object]
    # @yieldparam [RDF::Value] object
    # @return [Enumerator]
    # @see    RDF::Repository#each_object?
    def each_object(&block)
      if block_given?
        client.select(:o, distinct: true).where([:s, :p, :o]).each_solution { |solution| block.call(solution[:o]) }
      end
      enum_object
    end

    ##
    # Returns `true` if this repository contains the given `triple`.
    #
    # @param  [Array<RDF::Resource, RDF::URI, RDF::Value>] triple
    # @return [Boolean]
    # @see    RDF::Repository#has_triple?
    def has_triple?(triple)
      client.ask.whether(triple.to_a[0...3]).true?
    end

    ##
    # Returns `true` if this repository contains the given `statement`.
    #
    # @param  [RDF::Statement] statement
    # @return [Boolean]
    # @see    RDF::Repository#has_statement?
    def has_statement?(statement)
      has_triple?(statement.to_triple)
    end

    ##
    # Returns the number of statements in this repository.
    #
    # @return [Integer]
    # @see    RDF::Repository#count?
    def count
      begin
        binding = client.query("SELECT (COUNT(*) AS ?count) WHERE { ?s ?p ?o }").first.to_h
        binding[:count].value.to_i rescue 0
      rescue SPARQL::Client::MalformedQuery => e
        # SPARQL 1.0 does not include support for aggregate functions:
        each_statement.count
      end
    end

    alias_method :size,   :count
    alias_method :length, :count

    ##
    # Returns `true` if this repository contains no statements.
    #
    # @return [Boolean]
    # @see    RDF::Repository#empty?
    def empty?
      client.ask.whether([:s, :p, :o]).false?
    end

    ##
    # Returns `false` to indicate that this is a read-only repository.
    #
    # @return [Boolean]
    # @see    RDF::Mutable#mutable?
    def writable?
      true
    end

    ##
    # @private
    # @see RDF::Mutable#clear
    def clear_statements
      update_client.clear(:all)
    end

    ##
    # Deletes RDF statements from `self`.
    # If any statement contains an `RDF::Query::Variable`, it is
    # considered to be a pattern, and used to query
    # self to find matching statements to delete.
    #
    # @overload delete(*statements)
    #   @param  [Array<RDF::Statement>] statements
    #   @raise  [TypeError] if `self` is immutable
    #   @return [self]
    #
    # @overload delete(statements)
    #   @param  [Enumerable<RDF::Statement>] statements
    #   @raise  [TypeError] if `self` is immutable
    #   @return [self]
    #
    # @see RDF::Mutable#delete
    def delete(*statements)
      statements.map! do |value|
        if value.respond_to?(:each_statement)
          delete_statements(value)
          nil
        else
          value
        end
      end
      statements.compact!
      delete_statements(statements) unless statements.empty?
      return self
    end

    protected

    ##
    # Queries `self` using the given basic graph pattern (BGP) query,
    # yielding each matched solution to the given block.
    #
    # Overrides Queryable::query_execute to use SPARQL::Client::query
    #
    # @param  [RDF::Query] query
    #   the query to execute
    # @param  [Hash{Symbol => Object}] options ({})
    #   Any other options passed to `query.execute`
    # @yield  [solution]
    # @yieldparam  [RDF::Query::Solution] solution
    # @yieldreturn [void] ignored
    # @return [void] ignored
    # @see    RDF::Queryable#query
    # @see    RDF::Query#execute
    def query_execute(query, **options, &block)
      return nil unless block_given?
      q = SPARQL::Client::Query.
        select(query.variables, **{}).
        where(*query.patterns)
      client.query(q, **options).each do |solution|
        yield solution
      end
    end

    ##
    # Queries `self` for RDF statements matching the given `pattern`.
    #
    # @example
    #     repository.query([nil, RDF::DOAP.developer, nil])
    #     repository.query({predicate: RDF::DOAP.developer})
    #
    # @todo This should use basic SPARQL query mechanism.
    #
    # @param  [Pattern] pattern
    # @see    RDF::Queryable#query_pattern
    # @yield  [statement]
    # @yieldparam [Statement]
    # @return [Enumerable<Statement>]
    def query_pattern(pattern, **options, &block)
      pattern = pattern.dup
      pattern.subject   ||= RDF::Query::Variable.new
      pattern.predicate ||= RDF::Query::Variable.new
      pattern.object    ||= RDF::Query::Variable.new
      pattern.initialize!
      query = client.construct(pattern).where(pattern)

      if block_given?
        query.each_statement(&block)
      else
        query.solutions.to_a.extend(RDF::Enumerable, RDF::Queryable)
      end
    end

    ##
    # Deletes the given RDF statements from the underlying storage.
    #
    # Overridden here to use SPARQL/UPDATE
    #
    # @param  [RDF::Enumerable] statements
    # @return [void]
    def delete_statements(statements)
      constant = statements.all? do |value|
        # needs to be flattened... urgh
        !value.respond_to?(:each_statement) && begin
          statement = RDF::Statement.from(value)
          statement.constant? && !statement.has_blank_nodes?
        end
      end

      if constant
        update_client.delete_data(statements)
      else
        update_client.delete_insert(statements)
      end
    end

    ##
    # Inserts the given RDF statements into the underlying storage or output
    # stream.
    #
    # Overridden here to use SPARQL/UPDATE
    #
    # @param  [RDF::Enumerable] statements
    # @return [void]
    # @since  0.1.6
    def insert_statements(statements)
      raise ArgumentError, "Some statement is incomplete" if statements.any?(&:incomplete?)
      update_client.insert_data(statements)
    end

    ##
    # @private
    # @see RDF::Mutable#insert
    def insert_statement(statement)
      raise ArgumentError, "Statement #{statement.inspect} is incomplete" if statement.incomplete?
      update_client.insert_data([statement])
    end

  end
end


text:
class SPARQL::Client
  module VERSION
    FILE = File.expand_path('../../../../VERSION', __FILE__)
    MAJOR, MINOR, TINY, EXTRA = File.read(FILE).chomp.split('.')
    STRING = [MAJOR, MINOR, TINY, EXTRA].compact.join('.').freeze

    ##
    # @return [String]
    def self.to_s() STRING end

    ##
    # @return [String]
    def self.to_str() STRING end

    ##
    # @return [Array(Integer, Integer, Integer)]
    def self.to_a() [MAJOR, MINOR, TINY] end
  end
end


text:
class SPARQL::Client
  ##
  # SPARQL 1.1 Update operation builders.
  module Update
    ##
    # Insert statements into the graph
    #
    # @example INSERT DATA \{ <http://example.org/jhacker> <http://xmlns.com/foaf/0.1/name> \"J. Random Hacker\" .\}
    #   data = RDF::Graph.new do |graph|
    #     graph << [RDF::URI('http://example.org/jhacker'), RDF::Vocab::FOAF.name, "J. Random Hacker"]
    #   end
    #   insert_data(data)
    #
    # @example INSERT DATA \{ GRAPH <http://example.org/> \{\}\}
    #   insert_data(RDF::Graph.new, graph: 'http://example.org/')
    #   insert_data(RDF::Graph.new).graph('http://example.org/')
    #
    # @param (see InsertData#initialize)
    def self.insert_data(*arguments, **options)
      InsertData.new(*arguments, **options)
    end

    ##
    # Delete statements from the graph
    #
    # @example DELETE DATA \{ <http://example.org/jhacker> <http://xmlns.com/foaf/0.1/name> \"J. Random Hacker\" .\}
    #   data = RDF::Graph.new do |graph|
    #     graph << [RDF::URI('http://example.org/jhacker'), RDF::Vocab::FOAF.name, "J. Random Hacker"]
    #   end
    #   delete_data(data)
    #
    # @example DELETE DATA \{ GRAPH <http://example.org/> \{\}\}
    #   delete_data(RDF::Graph.new, graph: 'http://example.org/')
    #   delete_data(RDF::Graph.new).graph('http://example.org/')
    #
    # @param (see DeleteData#initialize)
    def self.delete_data(*arguments, **options)
      DeleteData.new(*arguments, **options)
    end

    ##
    # Load statements into the graph
    #
    # @example LOAD <http://example.org/data.rdf>
    #   load(RDF::URI(http://example.org/data.rdf))
    #
    # @example LOAD SILENT <http://example.org/data.rdf>
    #   load(RDF::URI(http://example.org/data.rdf)).silent
    #   load(RDF::URI(http://example.org/data.rdf), silent: true)
    #
    # @example LOAD <http://example.org/data.rdf> INTO <http://example.org/data.rdf>
    #   load(RDF::URI(http://example.org/data.rdf)).into(RDF::URI(http://example.org/data.rdf))
    #   load(RDF::URI(http://example.org/data.rdf), into: RDF::URI(http://example.org/data.rdf))
    #
    # @param (see Load#initialize)
    def self.load(*arguments, **options)
      Load.new(*arguments, **options)
    end

    ##
    # Clear the graph
    #
    # @example CLEAR GRAPH <http://example.org/data.rdf>
    #   clear.graph(RDF::URI(http://example.org/data.rdf))
    #   clear(:graph, RDF::URI(http://example.org/data.rdf))
    #
    # @example CLEAR DEFAULT
    #   clear.default
    #   clear(:default)
    #
    # @example CLEAR NAMED
    #   clear.named
    #   clear(:named)
    #
    # @example CLEAR ALL
    #   clear.all
    #   clear(:all)
    #
    # @example CLEAR SILENT ALL
    #   clear.all.silent
    #   clear(:all, silent: true)
    #
    # @param (see Clear#initialize)
    def self.clear(*arguments, **options)
      Clear.new(*arguments, **options)
    end

    ##
    # Create a graph
    #
    # @example CREATE GRAPH <http://example.org/data.rdf>
    #   create(RDF::URI(http://example.org/data.rdf))
    #
    # @example CREATE SILENT GRAPH <http://example.org/data.rdf>
    #   create(RDF::URI(http://example.org/data.rdf)).silent
    #   create(RDF::URI(http://example.org/data.rdf), silent: true)
    #
    # @param (see Create#initialize)
    def self.create(*arguments, **options)
      Create.new(*arguments, **options)
    end

    ##
    # Drop a graph
    #
    # @example DROP GRAPH <http://example.org/data.rdf>
    #   drop.graph(RDF::URI(http://example.org/data.rdf))
    #   drop(:graph, RDF::URI(http://example.org/data.rdf))
    #
    # @example DROP DEFAULT
    #   drop.default
    #   drop(:default)
    #
    # @example DROP NAMED
    #   drop.named
    #   drop(:named)
    #
    # @example DROP ALL
    #   drop.all
    #   drop(:all)
    #
    # @example DROP ALL SILENT
    #   drop.all.silent
    #   drop(:all, silent: true)
    #
    # @param (see Drop#initialize)
    def self.drop(*arguments, **options)
      Drop.new(*arguments, **options)
    end

    class Operation
      attr_reader :options

      def initialize(*arguments, **options)
        @options = options.dup
        unless arguments.empty?
          send(arguments.shift, *arguments)
        end
      end

      ##
      # Generic Update always returns statements
      #
      # @return [true]
      def expects_statements?
        true
      end

      ##
      # Set `silent` option
      def silent
        self.options[:silent] = true
        self
      end
    end

    ##
    # @see https://www.w3.org/TR/sparql11-update/#insertData
    class InsertData < Operation
      # @return [RDF::Enumerable]
      attr_reader :data

      ##
      # Insert statements into the graph
      #
      # @example INSERT DATA \{ <http://example.org/jhacker> <http://xmlns.com/foaf/0.1/name> \"J. Random Hacker\" .\}
      #   data = RDF::Graph.new do |graph|
      #     graph << [RDF::URI('http://example.org/jhacker'), RDF::Vocab::FOAF.name, "J. Random Hacker"]
      #   end
      #   insert_data(data)
      #
      # @param [Array<RDF::Statement>, RDF::Enumerable] data
      # @param  [Hash{Symbol => Object}] options
      def initialize(data, **options)
        @data = data
        super(**options)
      end

      ##
      # Cause data to be inserted into the graph specified by `uri`
      #
      # @param [RDF::URI] uri
      # @return [self]
      def graph(uri)
        self.options[:graph] = uri
        self
      end

      ##
      # InsertData always returns result set
      #
      # @return [true]
      def expects_statements?
        false
      end

      def to_s
        query_text = 'INSERT DATA {'
        query_text += ' GRAPH ' + SPARQL::Client.serialize_uri(self.options[:graph]) + ' {' if self.options[:graph]
        query_text += "\n"
        query_text += RDF::NTriples::Writer.buffer { |writer| @data.each { |d| writer << d } }
        query_text += '}' if self.options[:graph]
        query_text += "}\n"
      end
    end

    ##
    # @see https://www.w3.org/TR/sparql11-update/#deleteData
    class DeleteData < Operation
      # @return [RDF::Enumerable]
      attr_reader :data

      ##
      # Delete statements from the graph
      #
      # @example DELETE DATA \{ <http://example.org/jhacker> <http://xmlns.com/foaf/0.1/name> \"J. Random Hacker\" .\}
      #   data = RDF::Graph.new do |graph|
      #     graph << [RDF::URI('http://example.org/jhacker'), RDF::Vocab::FOAF.name, "J. Random Hacker"]
      #   end
      #   delete_data(data)
      #
      # @param [Array<RDF::Statement>, RDF::Enumerable] data
      # @param  [Hash{Symbol => Object}] options
      def initialize(data, **options)
        @data = data
        super(**options)
      end

      ##
      # Cause data to be deleted from the graph specified by `uri`
      #
      # @param [RDF::URI] uri
      # @return [self]
      def graph(uri)
        self.options[:graph] = uri
        self
      end

      def to_s
        query_text = 'DELETE DATA {'
        query_text += ' GRAPH ' + SPARQL::Client.serialize_uri(self.options[:graph]) + ' {' if self.options[:graph]
        query_text += "\n"
        query_text += RDF::NTriples::Writer.buffer { |writer| @data.each { |d| writer << d } }
        query_text += '}' if self.options[:graph]
        query_text += "}\n"
      end
    end

    ##
    # @see https://www.w3.org/TR/sparql11-update/#deleteInsert
    class DeleteInsert < Operation
      attr_reader :insert_graph
      attr_reader :delete_graph
      attr_reader :where_graph

      def initialize(_delete_graph, _insert_graph = nil, _where_graph = nil, **options)
        @delete_graph = _delete_graph
        @insert_graph = _insert_graph
        @where_graph = _where_graph
        super(**options)
      end

      ##
      # Cause data to be deleted and inserted from the graph specified by `uri`
      #
      # @param [RDF::URI] uri
      # @return [self]
      def graph(uri)
        self.options[:graph] = uri
        self
      end

      def to_s
        buffer = []

        if self.options[:graph]
          buffer << "WITH"
          buffer << SPARQL::Client.serialize_uri(self.options[:graph])
        end
        if delete_graph and !delete_graph.empty?
          serialized_delete = SPARQL::Client.serialize_patterns delete_graph, true
          buffer << "DELETE {\n"
          buffer += serialized_delete
          buffer << "}\n"
        end
        if insert_graph and !insert_graph.empty?
          buffer << "INSERT {\n"
          buffer += SPARQL::Client.serialize_patterns insert_graph, true
          buffer << "}\n"
        end
          buffer << "WHERE {\n"
        if where_graph
          buffer += SPARQL::Client.serialize_patterns where_graph, true
        elsif serialized_delete
          buffer += serialized_delete
        end
        buffer << "}\n"
        buffer.join(' ')
      end

    end

    ##
    # @see https://www.w3.org/TR/sparql11-update/#load
    class Load < Operation
      attr_reader :from
      attr_reader :into

      ##
      # Load statements into the graph
      #
      # @example LOAD <http://example.org/data.rdf>
      #   load(RDF::URI(http://example.org/data.rdf))
      #
      # @example LOAD SILENT<http://example.org/data.rdf>
      #   load(RDF::URI(http://example.org/data.rdf)).silent
      #   load(RDF::URI(http://example.org/data.rdf), silent: true)
      #
      # @example LOAD <http://example.org/data.rdf> INTO <http://example.org/data.rdf>
      #   load(RDF::URI(http://example.org/data.rdf)).into(RDF::URI(http://example.org/data.rdf))
      #   load(RDF::URI(http://example.org/data.rdf), into: RDF::URI(http://example.org/data.rdf))
      # @param [RDF::URI] from
      # @param  [Hash{Symbol => Object}] options
      # @option [RDF::URI] :into
      # @option [Boolean] :silent
      def initialize(from, into: nil,**options)
        @from = RDF::URI(from)
        @into = RDF::URI(into) if into
        super(**options)
      end

      ##
      # Cause data to be loaded into graph specified by `uri`
      #
      # @param [RDF::URI] uri
      # @return [self]
      def into(uri)
        @into = RDF::URI(uri)
        self
      end

      def to_s
        query_text = 'LOAD '
        query_text += 'SILENT ' if self.options[:silent]
        query_text += SPARQL::Client.serialize_uri(@from)
        query_text += ' INTO GRAPH ' + SPARQL::Client.serialize_uri(@into) if @into
        query_text
      end
    end

    ##
    # @see https://www.w3.org/TR/sparql11-update/#clear
    class Clear < Operation
      attr_reader :uri

      ##
      # Cause data to be cleared from graph specified by `uri`
      #
      # @param [RDF::URI] uri
      # @return [self]
      def graph(uri)
        @what, @uri = :graph, uri
        self
      end

      ##
      # Cause data to be cleared from the default graph
      #
      # @return [self]
      def default
        @what = :default
        self
      end

      ##
      # Cause data to be cleared from named graphs
      #
      # @return [self]
      def named
        @what = :named
        self
      end

      ##
      # Cause data to be cleared from all graphs
      #
      # @return [self]
      def all
        @what = :all
        self
      end

      ##
      # Clear always returns statements
      #
      # @return [false]
      def expects_statements?
        false
      end

      def to_s
        query_text = 'CLEAR '
        query_text += 'SILENT ' if self.options[:silent]
        case @what.to_sym
          when :graph   then query_text += 'GRAPH ' + SPARQL::Client.serialize_uri(@uri)
          when :default then query_text += 'DEFAULT'
          when :named   then query_text += 'NAMED'
          when :all     then query_text += 'ALL'
          else raise ArgumentError, "invalid CLEAR operation: #{@what.inspect}"
        end
        query_text
      end
    end

    ##
    # @see https://www.w3.org/TR/sparql11-update/#create
    class Create < Operation
      attr_reader :uri

      # @param  [Hash{Symbol => Object}] options
      def initialize(uri, **options)
        @uri = RDF::URI(uri)
        super(**options)
      end

      def to_s
        query_text = 'CREATE '
        query_text += 'SILENT ' if self.options[:silent]
        query_text += 'GRAPH ' + SPARQL::Client.serialize_uri(@uri)
        query_text
      end
    end

    ##
    # @see https://www.w3.org/TR/sparql11-update/#drop
    class Drop < Clear
      def to_s
        query_text = 'DROP '
        query_text += 'SILENT ' if self.options[:silent]
        case @what.to_sym
          when :graph   then query_text += 'GRAPH ' + SPARQL::Client.serialize_uri(@uri)
          when :default then query_text += 'DEFAULT'
          when :named   then query_text += 'NAMED'
          when :all     then query_text += 'ALL'
          else raise ArgumentError, "invalid DROP operation: #{@what.inspect}"
        end
        query_text
      end
    end

    ##
    # @see https://www.w3.org/TR/sparql11-update/#copy
    class Copy < Operation
      def to_s
        # TODO
      end
    end

    ##
    # @see https://www.w3.org/TR/sparql11-update/#move
    class Move < Operation
      def to_s
        # TODO
      end
    end

    ##
    # @see https://www.w3.org/TR/sparql11-update/#add
    class Add < Operation
      def to_s
        # TODO
      end
    end
  end # Update
end # SPARQL::Client


text:
#require 'rdf/turtle'
require 'sparql/client'

sparql = SPARQL::Client.new("http://data.americanartcollaborative.org/sparql")
uri = RDF.URI("http://data.crystalbridges.org/object/2258")
label = RDF::RDFS.label

query = sparql.construct([uri, label, :o]).where([uri, label, :o])
query.each_statement do |s|
  puts s.object.inspect
end

# <http://data.crystalbridges.org/object/2258> <http://www.w3.org/2000/01/rdf-schema#label> "Bison-Dance of the Mandan Indians in front of their Medicine Lodge in Mih-Tuta-Hankush" .
# <http://data.crystalbridges.org/object/2258> <http://www.w3.org/2000/01/rdf-schema#label> "From \"Voyage dans l’intérieur de l’Amérique du Nord, executé pendant les années 1832, 1833 et 1834, par le prince Maximilien de Wied-Neuwied\" (Paris & Coblenz, 1839-1843)" .

# "<http://data.crystalbridges.org/object/2258> <http://www.w3.org/2000/01/rdf-schema#label> \"Bison-Dance of the Mandan Indians in front of their Medicine Lodge in Mih-Tuta-Hankush\" .\n<http://data.crystalbridges.org/object/2258> <http://www.w3.org/2000/01/rdf-schema#label> \"From \\\"Voyage dans l\xE2\x80\x99int\xC3\xA9rieur de l\xE2\x80\x99Am\xC3\xA9rique du Nord, execut\xC3\xA9 pendant les ann\xC3\xA9es 1832, 1833 et 1834, par le prince Maximilien de Wied-Neuwied\\\" (Paris & Coblenz, 1839-1843)\" .\n"


text:
require 'sparql/client'
SPARQL::Client::Query
  .select
  .where(%i[s p o])
  .values(:s, RDF::URI('http://example.com/1'), RDF::URI('http://example.com/2'))


text:
require 'rdf'
require 'sparql/client'
dnbt = RDF::Vocabulary.new("https://d-nb.info/standards/elementset/dnb#")
rdf_gndid = RDF::Literal.new("https://d-nb.info/gnd/1059461498")
sparql_client = SPARQL::Client.new("http://127.0.0.1:9292/")
gndo = RDF::Vocabulary.new("http://d-nb.info/gnd/standards/elementset/gnd#")
query = sparql_client.select.where([:subject, dnbt.deprecatedUri, rdf_gndid]).where([:subject, gndo.gndIdentifier, :gndid])
query.each_solution { |solution| new_gndid = solution[:gndid].to_s }