Sponsoring The Perl Toolchain Summit 2025: Help make this important event another success Learn more

use v5.14;
=head1 NAME
Attean::TripleModel - RDF model backed by a set of triple-stores
=head1 VERSION
This document describes Attean::TripleModel version 0.035
=head1 SYNOPSIS
use v5.14;
use Attean;
my $model = Attean::TripleModel->new( stores => {
} );
=head1 DESCRIPTION
The Attean::TripleModel class represents a model that is backed by a set of
L<Attean::API::TripleStore|Attean::API::Store> objects, identified by an IRI
string. It conforms to the L<Attean::API::Model> role.
The Attean::TripleModel constructor requires one named argument:
=over 4
=item stores
A hash mapping graph IRI values to L<Attean::API::TripleStore|Attean::API::Store>
objects representing the backing triple-store for that graph.
=back
=head1 METHODS
=over 4
=cut
package Attean::TripleModel 0.035 {
use Moo;
use Types::Standard qw(ArrayRef ConsumerOf HashRef);
use Scalar::Util qw(reftype blessed);
has 'stores' => (
is => 'ro',
isa => HashRef[ConsumerOf['Attean::API::TripleStore']],
required => 1,
default => sub { +{} },
);
=item C<< size >>
=cut
sub size {
my $self = shift;
my $count = 0;
foreach my $store (values %{ $self->stores }) {
$count += $store->size;
}
return $count;
}
=item C<< count_quads >>
=cut
sub count_quads {
my $self = shift;
# TODO: don't materialize results here just to count them
my $iter = $self->get_quads( @_ );
my $count = 0;
while (my $r = $iter->next) {
$count++;
}
return $count;
}
=item C<< count_quads_estimate >>
=cut
sub count_quads_estimate {
my $self = shift;
my ($s, $p, $o, $g) = @_;
if (blessed($g) and $g->does('Attean::API::IRI')) {
if (my $store = $self->stores->{ $g->value }) {
return $store->count_quads_estimate(@_);
} else {
return 0;
}
} else {
return $self->count_quads(@_);
}
}
=item C<< holds >>
=cut
sub holds {
my $self = shift;
return ($self->count_quads_estimate(@_) > 0)
}
=item C<< get_graphs >>
=cut
sub get_graphs {
my $self = shift;
my @graphs = map { Attean::IRI->new($_) } keys %{ $self->stores };
return Attean::ListIterator->new( values => \@graphs, item_type => 'Attean::API::Term' );
}
=item C<< get_quads ( $subject, $predicate, $object, $graph ) >>
Returns an L<Attean::API::Iterator> for quads in the model that match the
supplied C<< $subject >>, C<< $predicate >>, C<< $object >>, and C<< $graph >>.
Any of these terms may be undefined or a L<Attean::API::Variable> object, in
which case that term will be considered as a wildcard for the purposes of
matching.
The returned iterator conforms to both L<Attean::API::Iterator> and
L<Attean::API::QuadIterator>.
=cut
sub get_quads {
my $self = shift;
my @nodes = @_[0..3];
foreach my $i (0..3) {
my $t = $nodes[$i] // Attean::Variable->new();
if (not(ref($t)) or reftype($t) ne 'ARRAY') {
$nodes[$i] = [$t];
}
}
my @iters;
foreach my $s (@{ $nodes[0] }) {
foreach my $p (@{ $nodes[1] }) {
foreach my $o (@{ $nodes[2] }) {
foreach my $g (@{ $nodes[3] }) {
my $iter = $self->_get_quads($s, $p, $o, $g);
push(@iters, $iter);
}
}
}
}
if (scalar(@iters) <= 1) {
return shift(@iters);
} else {
return Attean::IteratorSequence->new( iterators => \@iters, item_type => $iters[0]->item_type );
}
}
sub _get_quads {
my $self = shift;
my $s = shift;
my $p = shift;
my $o = shift;
my $g = shift;
if (blessed($g) and $g->does('Attean::API::IRI')) {
if (my $store = $self->stores->{ $g->value }) {
my $iter = $store->get_triples($s, $p, $o);
return $iter->as_quads($g);
}
} elsif (blessed($g) and $g->does('Attean::API::Variable')) {
my @iters;
while (my ($g, $store) = each %{ $self->stores }) {
my $iter = $store->get_triples($s, $p, $o);
my $graph = Attean::IRI->new($g);
my $quads = $iter->map(sub { $_->as_quad($graph) }, 'Attean::API::Quad');
push(@iters, $quads);
}
my $iter = Attean::IteratorSequence->new( iterators => \@iters, item_type => $iters[0]->item_type );
return $iter;
} else {
my $name = (blessed($g) and $g->can('as_string')) ? $g->as_string : "$g";
$self->log->warn("TripleModel cannot produce quads for non-IRI graph: $name");
}
return Attean::ListIterator->new( values => [], item_type => 'Attean::API::Quad' );
}
=item C<< plans_for_algebra( $algebra, $planner, $active_graphs, $default_graphs ) >>
Delegates to an underlying store if the active graph is bound to the store,
and the store consumes Attean::API::CostPlanner.
=cut
sub plans_for_algebra {
my $self = shift;
my $algebra = shift;
my $planner = shift;
my $active_graphs = shift;
my $default_graphs = shift;
my @plans;
if (scalar(@$active_graphs) == 1) {
my $graph = $active_graphs->[0];
if (my $store = $self->stores->{ $graph->value }) {
if ($store->does('Attean::API::CostPlanner')) {
push(@plans, $store->plans_for_algebra($algebra, $planner, $active_graphs, $default_graphs));
}
}
}
return @plans;
}
=item C<< cost_for_plan( $plan ) >>
Attempts to delegate to all the underlying stores if that store consumes Attean::API::CostPlanner.
=cut
sub cost_for_plan {
my $self = shift;
my $plan = shift;
foreach my $store (values %{ $self->stores }) {
if ($store->does('Attean::API::CostPlanner')) {
if (defined(my $cost = $store->cost_for_plan($plan, @_))) {
return $cost;
}
}
}
return;
}
}
use Scalar::Util qw(blessed);
use Types::Standard qw(CodeRef);
use Moo::Role;
has 'store_constructor' => (is => 'ro', isa => CodeRef, required => 1);
=item C<< add_store( $graph => $store ) >>
Add the L<Attean::TripleStore> C<< $store >> object to the model using the
IRI string value C<< $graph >> as the graph name.
=cut
sub add_store {
my $self = shift;
my $graph = shift;
my $iri = blessed($graph) ? $graph->value : $graph;
my $store = shift;
die if exists $self->stores->{ $iri };
$self->stores->{ $iri } = $store;
}
=item C<< create_graph( $graph ) >>
Create a new L<Attean::TripleStore> and add it to the model using the
L<Attean::API::IRI> C<< $graph >> as the graph name.
The store is constructed by using this object's C<< store_constructor >>
attribute:
my $store = $self->store_constructor->($graph);
=cut
sub create_graph {
my $self = shift;
my $graph = shift;
my $iri = $graph->value;
return if exists $self->stores->{ $iri };
my $store = $self->store_constructor->($graph);
$self->stores->{ $iri } = $store;
};
=item C<< drop_graph( $graph ) >>
Removes the store associated with the given C<< $graph >>.
=cut
sub drop_graph {
my $self = shift;
my $g = shift;
if ($g->does('Attean::API::IRI')) {
delete $self->stores->{ $g->value };
}
}
}
use Moo;
use Types::Standard qw(ArrayRef ConsumerOf HashRef);
use Scalar::Util qw(reftype);
has 'stores' => (
is => 'ro',
isa => HashRef[ConsumerOf['Attean::API::MutableTripleStore']],
required => 1,
default => sub { +{} },
);
=item C<< add_quad ( $quad ) >>
Adds the specified C<$quad> to the underlying model.
=cut
sub add_quad {
my $self = shift;
my $q = shift;
my $g = $q->graph;
die "Cannot add a quad whose graph is not an IRI" unless ($g->does('Attean::API::IRI'));
my $v = $g->value;
if (my $store = $self->stores->{ $v }) {
$store->add_triple( $q->as_triple );
} else {
Carp::confess "No such graph: $v";
}
}
=item C<< remove_quad ( $quad ) >>
Removes the specified C<< $quad >> from the underlying store.
=cut
sub remove_quad {
my $self = shift;
my $q = shift;
my $g = $q->graph;
if ($g->does('Attean::API::IRI')) {
my $v = $g->value;
if (my $store = $self->stores->{ $v }) {
$store->remove_triple( $q->as_triple );
}
}
}
sub create_graph { die; }
=item C<< drop_graph( $graph ) >>
Removes the store associated with the given C<< $graph >>.
=cut
sub drop_graph {
my $self = shift;
my $g = shift;
if ($g->does('Attean::API::IRI')) {
delete $self->stores->{ $g->value };
}
}
=item C<< clear_graph( $graph ) >>
Removes all quads with the given C<< $graph >>.
=cut
sub clear_graph {
my $self = shift;
my $g = shift;
$self->drop_graph($g);
$self->create_graph($g);
}
}
use Moo;
use Scalar::Util qw(blessed);
use Types::Standard qw(CodeRef);
}
use Moo;
use Scalar::Util qw(blessed);
use Types::Standard qw(CodeRef);
}
1;
__END__
=back
=head1 BUGS
Please report any bugs or feature requests to through the GitHub web interface
=head1 SEE ALSO
=head1 AUTHOR
Gregory Todd Williams C<< <gwilliams@cpan.org> >>
=head1 COPYRIGHT
Copyright (c) 2014--2022 Gregory Todd Williams.
This program is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.
=cut