Commits

Toby Inkster  committed 2003559

tidy up repo

  • Participants

Comments (0)

Files changed (14)

+############################################################################
+## Changes #################################################################
+############################################################################
+
+RDF-Trine-Store-Tardis
+======================
+
+Created:      2011-09-15
+Home page:    <https://metacpan.org/release/RDF-Trine-Store-Tardis>
+Bug tracker:  <http://rt.cpan.org/Dist/Display.html?Queue=RDF-Trine-Store-Tardis>
+Maintainer:   Toby Inkster <mailto:tobyink@cpan.org>
+
+0.001 [2012-01-01] # Initial release
+
+#############################################################
+
+@prefix :        <http://usefulinc.com/ns/doap#> .
+@prefix dcs:     <http://ontologi.es/doap-changeset#> .
+@prefix dc:      <http://purl.org/dc/terms/> .
+@prefix foaf:    <http://xmlns.com/foaf/0.1/> .
+@prefix my:      <http://purl.org/NET/cpan-uri/dist/RDF-Trine-Store-Tardis/> .
+@prefix rdfs:    <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix toby:    <http://tobyinkster.co.uk/#> .
+@prefix xsd:     <http://www.w3.org/2001/XMLSchema#> .
+
+#############################################################
+
+<>
+
+	dc:title         "Changes" ;
+	dc:description   "Revision history for Perl extension RDF-Trine-Store-Tardis."@en ;
+	dc:subject       my:project ;
+	dc:creator       toby:i .
+
+#############################################################
+
+my:v_0-001
+
+	a               :Version ;
+	dc:issued       "2012-01-01"^^xsd:date ;
+	:revision       "0.001"^^xsd:string ;
+	:file-release   <http://backpan.cpan.org/authors/id/T/TO/TOBYINK/RDF-Trine-Store-Tardis-0.001.tar.gz> ;
+	rdfs:label      "Initial release"@en .
+
+#############################################################
+
+my:project
+
+	a               :Project ;
+	:name           "RDF-Trine-Store-Tardis" ;
+	:shortdesc      "Time travelling triple store."@en ;
+	:programming-language  "Perl" ;
+	:homepage       <https://metacpan.org/release/RDF-Trine-Store-Tardis> ;
+	:download-page  <https://metacpan.org/release/RDF-Trine-Store-Tardis> ;
+	:bug-database   <http://rt.cpan.org/Dist/Display.html?Queue=RDF-Trine-Store-Tardis> ;
+	:repository     [ a :SVNRepository ; :browse <http://goddamn.co.uk/svn-web/perlmods/browse/RDF-Trine-Store-Tardis/> ] ;
+	:maintainer     toby:i ;
+	:developer      toby:i ;
+	:documenter     toby:i ;
+	:tester         toby:i ;
+	:created        "2011-09-15"^^xsd:date ;
+	:license        <http://dev.perl.org/licenses/> ;
+	:release        my:v_0-001 .
+
+#############################################################
+
+toby:i
+
+	a               foaf:Person ;
+	foaf:name       "Toby Inkster" ;
+	foaf:homepage   <http://tobyinkster.co.uk/> ;
+	foaf:page       <https://metacpan.org/author/TOBYINK> ;
+	foaf:mbox       <mailto:tobyink@cpan.org> ;
+	<http://www.w3.org/2002/07/owl#sameAs> <http://purl.org/NET/cpan-uri/person/tobyink> .
+
+#############################################################
+<?xml version="1.0" encoding="utf-8"?>
+<rdf:RDF xmlns:dbug="http://ontologi.es/doap-bugs#" xmlns:dc="http://purl.org/dc/terms/" xmlns:dcs="http://ontologi.es/doap-changeset#" xmlns:doap="http://usefulinc.com/ns/doap#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
+<rdf:Description rdf:nodeID="rF6BDD9A8E2A911E09409B834E6AB9F36r0">
+	<doap:browse rdf:resource="http://goddamn.co.uk/svn-web/perlmods/browse/RDF-Trine-Store-Tardis/"/>
+	<rdf:type rdf:resource="http://usefulinc.com/ns/doap#SVNRepository"/>
+</rdf:Description>
+<rdf:Description rdf:about="file:///home/tai/src/perlmods/RDF-Trine-Store-Tardis/Changes.ttl">
+	<dc:creator rdf:resource="http://tobyinkster.co.uk/#i"/>
+	<dc:description xml:lang="en">Revision history for Perl extension RDF-Trine-Store-Tardis.</dc:description>
+	<dc:subject rdf:resource="http://purl.org/NET/cpan-uri/dist/RDF-Trine-Store-Tardis/project"/>
+	<dc:title>Changes</dc:title>
+</rdf:Description>
+<rdf:Description rdf:about="http://purl.org/NET/cpan-uri/dist/RDF-Trine-Store-Tardis/project">
+	<doap:bug-database rdf:resource="http://rt.cpan.org/Dist/Display.html?Queue=RDF-Trine-Store-Tardis"/>
+	<doap:created rdf:datatype="http://www.w3.org/2001/XMLSchema#date">2011-09-15</doap:created>
+	<doap:developer rdf:resource="http://tobyinkster.co.uk/#i"/>
+	<doap:documenter rdf:resource="http://tobyinkster.co.uk/#i"/>
+	<doap:download-page rdf:resource="https://metacpan.org/release/RDF-Trine-Store-Tardis"/>
+	<doap:homepage rdf:resource="https://metacpan.org/release/RDF-Trine-Store-Tardis"/>
+	<doap:license rdf:resource="http://dev.perl.org/licenses/"/>
+	<doap:maintainer rdf:resource="http://tobyinkster.co.uk/#i"/>
+	<doap:name>RDF-Trine-Store-Tardis</doap:name>
+	<doap:programming-language>Perl</doap:programming-language>
+	<doap:release rdf:resource="http://purl.org/NET/cpan-uri/dist/RDF-Trine-Store-Tardis/v_0-001"/>
+	<doap:repository rdf:nodeID="rF6BDD9A8E2A911E09409B834E6AB9F36r0"/>
+	<doap:shortdesc xml:lang="en">Time travelling triple store.</doap:shortdesc>
+	<doap:tester rdf:resource="http://tobyinkster.co.uk/#i"/>
+	<rdf:type rdf:resource="http://usefulinc.com/ns/doap#Project"/>
+</rdf:Description>
+<rdf:Description rdf:about="http://purl.org/NET/cpan-uri/dist/RDF-Trine-Store-Tardis/v_0-001">
+	<dc:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#date">2012-01-01</dc:issued>
+	<doap:file-release rdf:resource="http://backpan.cpan.org/authors/id/T/TO/TOBYINK/RDF-Trine-Store-Tardis-0.001.tar.gz"/>
+	<doap:revision rdf:datatype="http://www.w3.org/2001/XMLSchema#string">0.001</doap:revision>
+	<rdf:type rdf:resource="http://usefulinc.com/ns/doap#Version"/>
+	<rdfs:label xml:lang="en">Initial release</rdfs:label>
+</rdf:Description>
+<rdf:Description xmlns:ns1="http://www.w3.org/2002/07/owl#" rdf:about="http://tobyinkster.co.uk/#i">
+	<rdf:type rdf:resource="http://xmlns.com/foaf/0.1/Person"/>
+	<ns1:sameAs rdf:resource="http://purl.org/NET/cpan-uri/person/tobyink"/>
+	<foaf:homepage rdf:resource="http://tobyinkster.co.uk/"/>
+	<foaf:mbox rdf:resource="mailto:tobyink@cpan.org"/>
+	<foaf:name>Toby Inkster</foaf:name>
+	<foaf:page rdf:resource="https://metacpan.org/author/TOBYINK"/>
+</rdf:Description>
+</rdf:RDF>

File MANIFEST.SKIP

+^MANIFEST.bak$
+^Makefile.old$
+^Makefile$
+^blib/
+^pm_to_blib
+^blibdirs
+\.svn
+^example.*\.pl$
+^[^/]+\.(tar\.gz|tar\.bz2|tgz|tbz2|tbz|zip|tar)$
+^MYMETA.yml
+---
+abstract: 'time travelling triple store'
+author:
+  - 'Toby Inkster <tobyink@cpan.org>'
+build_requires:
+  ExtUtils::MakeMaker: 6.42
+  Test::More: 0.61
+configure_requires:
+  ExtUtils::MakeMaker: 6.42
+distribution_type: module
+generated_by: 'Module::Install version 1.00'
+keywords:
+  - versioning
+  - versioned
+  - datetime
+  - date
+  - time
+  - RDF
+  - SPARQL
+  - 'Semantic Web'
+license: perl
+meta-spec:
+  url: http://module-build.sourceforge.net/META-spec-v1.4.html
+  version: 1.4
+name: RDF-Trine-Store-Tardis
+no_index:
+  directory:
+    - inc
+    - t
+requires:
+  Carp: 0
+  DBI: 0
+  DateTime: 0
+  DateTime::Format::W3CDTF: 0
+  RDF::Trine: 0.133
+  Scalar::Util: 0
+  common::sense: 0
+  mro: 0
+  perl: 5.10.0
+resources:
+  bugtracker: http://rt.cpan.org/Dist/Display.html?Queue=RDF-Trine-Store-Tardis
+  homepage: https://metacpan.org/release/RDF-Trine-Store-Tardis
+  license: http://dev.perl.org/licenses/
+  repository: http://goddamn.co.uk/svn-web/perlmods/browse/RDF-Trine-Store-Tardis/
+version: 0.000_01
+use strict;
+use warnings;
+
+use inc::Module::Install;
+
+my $dist = 'RDF-Trine-Store-Tardis';
+my $fn   = "lib/$dist.pm"; $fn =~ s#-#/#g;
+
+name                $dist;
+perl_version_from   $fn;
+version_from        $fn;
+abstract_from       $fn;
+readme_from         $fn;
+author              'Toby Inkster <tobyink@cpan.org>';
+license             'perl';
+
+requires            'Carp'               => 0;
+requires            'common::sense'      => 0;
+requires            'DateTime'           => 0;
+requires            'DateTime::Format::W3CDTF' => 0;
+requires            'DBI'                => 0;
+requires            'mro'                => 0;
+requires            'RDF::Trine'         => '0.133';
+requires            'Scalar::Util'       => 0;
+test_requires       'Test::More'         => '0.61';
+
+resources(
+	'homepage'   => "https://metacpan.org/release/$dist",
+	'repository' => "http://goddamn.co.uk/svn-web/perlmods/browse/$dist/",
+	'bugtracker' => "http://rt.cpan.org/Dist/Display.html?Queue=$dist",
+	);
+
+keywords('versioning', 'versioned', 'datetime', 'date', 'time', 'RDF', 'SPARQL', 'Semantic Web');
+
+write_doap_changes;
+write_doap_changes_xml;
+auto_license;
+auto_manifest;
+
+auto_install;
+WriteAll(
+	'meta' => 1,
+	'sign' => 1,
+	);
+NAME
+    RDF::Trine::Store::Tardis - time travelling triple store
+
+SYNOPSIS
+     use DateTime;
+     use RDF::Trine qw[iri statement];
+     use RDF::Trine::Store::Tardis;
+ 
+     my $S = iri('http://example.com/s');
+     my $P = iri('http://example.com/p');
+     my $O = iri('http://example.com/o');
+ 
+     my $store = RDF::Trine::Store::Tardis->new;
+ 
+     $store->add(statement($S, $P, $O));
+     sleep 5;
+ 
+     $store->remove(statement($S, $P, $O));
+     sleep 1;
+ 
+     my $then     = DateTime->now->subtract(seconds => 3);
+     my $then_iri = $store->tardis_make_iri($then);
+     my $results  = $store->get_statements(undef, undef, undef, $then_iri);
+     while (my $result = $results->next)
+     {
+       print $result->as_string . "\n";
+     }
+
+DESCRIPTION
+    Tempus is a triple store (not a quad store!) that keeps track of when
+    triples were added and removed from the graph.
+
+  Constructor
+    "new([$name])"
+        Constructs a temporary store.
+
+    "new($name, $dbh)"
+        Uses an existing DBI database connection.
+
+    "new($name, $dsn, $username, $password)"
+        Opens a DBI database connection using supplied DSN, username and
+        password.
+
+    Note that while it's not especially feasible to share the same database
+    and model name between Tardis and RDF::Trine::Store::DBI, the latter
+    should be able to safely *read* a Tardis store.
+
+  Methods
+    Tempus inherits from RDF::Trine::Store so provides all the methods of an
+    RDF::Trine::Store. The following additional methods and differences are
+    noteworthy:
+
+    "add_statement($statement)"
+    "remove_statement($statement)"
+    "remove_statements($subj, $pred, $obj)"
+        Can only be handle a triple, not a quad. If passed a quad, a warning
+        is given and the context is ignored.
+
+    "get_statements($subj, $pred, $obj)"
+        Ignores historic and futuristic triples, only returning triples that
+        are valid at the current time.
+
+    "get_statements($subj, $pred, $obj, $context)"
+        $context must be an IRI if the form <tempus:2003-02-01T12:00:00Z> or
+        a DateTime object. Returns triples valid at that time.
+
+    "get_statements($subj, $pred, $obj, undef)"
+        Returns current, historic and future triples.
+
+    "tardis_add_statement($triple, $datetime)"
+    "tardis_remove_statement($triple, $datetime)"
+        Add or remove a statement at a time other than now. $datetime can be
+        a special tempus URI, or a "DateTime" object.
+
+    "tardis_add_statement($quad)"
+    "tardis_remove_statement($quad)"
+        Quads are supported for these methods, provided that the context is
+        a special tempus URI.
+
+    "tardis_make_iri($datetime)"
+        Given a "DateTime" object, returns an RDF::Trine::Node::Resource for
+        a special tempus URI representing that time.
+
+    "tardis_parse_iri_to_datetime($resource)"
+        Does the inverse of "tardis_make_iri".
+
+  Tempus URIs
+    Here is the EBNF notation for tempus URIs:
+
+     tempusURI = 'tempus:', ('now' | date | dateTime);
+     dateTime  = date, 'T', time;
+     date      = year, '-', month, '-', day;
+     time      = hour, ':', minute, [':', second,] [timezone];
+     year      = digit, digit, digit, digit;
+     month     = digit, digit;
+     day       = digit, digit;
+     hour      = digit, digit;
+     minute    = digit, digit;
+     second    = digit, digit;
+     timezone  = 'Z' | numTZ
+     numTZ     = ( '+' | '-' ), digit, digit, digit, digit;
+     digit     = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
+
+BUGS
+    I imagine there are many.
+
+    Please report any bugs to <http://rt.cpan.org/>.
+
+SEE ALSO
+    RDF::Trine, DateTime.
+
+    RDF::Trine::Store::Tardis::SQLite, RDF::Trine::Store::Tardis::mysql,
+    RDF::Trine::Store::Tardis::Pg.
+
+    <http://www.perlrdf.org/>.
+
+    <http://en.wikipedia.org/wiki/TARDIS>.
+
+AUTHOR
+    Toby Inkster <tobyink@cpan.org>.
+
+COPYRIGHT
+    Copyright 2011 Toby Inkster
+
+    This library is free software; you can redistribute it and/or modify it
+    under the same terms as Perl itself.
+
+- Test get_pattern stuff.

File example-prime-ministers.pl

+#!/usr/bin/perl
+
+use lib "lib";
+use RDF::Query;
+use RDF::Trine qw[iri literal blank statement];
+use RDF::Trine::Store::Tardis;
+use DBI;
+
+my $file  = shift @ARGV
+	or die "usage: test.pl filename [init]\n";
+my $store = RDF::Trine::Store::Tardis->new('model',
+	DBI->connect("dbi:SQLite:dbname=${file}"));
+
+my @PMs = split /\r?\n/, <<PMS;
+Edward Heath	1970-06-19	1974-03-04
+Harold Wilson	1974-03-04	1976-04-05
+James Callaghan	1976-04-05	1979-05-04
+Margaret Thatcher	1979-05-04	1990-11-28
+John Major	1990-11-28	1997-05-02
+Tony Blair	1997-05-02	2007-06-27
+Gordon Brown	2007-06-27	2010-05-11
+David Cameron	2010-05-11
+PMS
+
+if (shift(@ARGV) =~ /init/)
+{
+	foreach my $pm (@PMs)
+	{
+		my ($name, $start, $end) = split /\t/, $pm;
+		$store->tardis_add_statement(statement(
+			iri('http://ontologi.es/place/GB'),
+			iri('http://example.com/terms#prime_minister'),
+			literal($name),
+			iri(sprintf('tempus:%s', $start)),
+			));
+		$store->tardis_remove_statement(statement(
+			iri('http://ontologi.es/place/GB'),
+			iri('http://example.com/terms#prime_minister'),
+			literal($name),
+			iri(sprintf('tempus:%s', $end)),
+			)) if $end;
+	}
+}
+
+my $iter = $store->get_statements(undef, undef, undef, iri('tempus:1995-01-01'));
+while (my $st = $iter->next)
+{
+	print $st->as_string . "\n";
+}
+print "----\n";
+my $iter = $store->get_statements(undef, undef, undef, iri('tempus:now'));
+while (my $st = $iter->next)
+{
+	print $st->as_string . "\n";
+}
+print "----\n";
+my $iter = $store->get_statements(undef, undef, undef, undef);
+while (my $st = $iter->next)
+{
+	print $st->as_string . "\n";
+}
+print "----\n";
+my $iter = $store->get_statements(undef, undef, undef);
+while (my $st = $iter->next)
+{
+	print $st->as_string . "\n";
+}
+print "----\n";
+
+my $sparql = RDF::Query->new(<<SPARQL);
+SELECT ?g ?oldpm ?newpm
+WHERE {
+	GRAPH <tempus:2011-09-01>
+		{ ?gb <http://example.com/terms#prime_minister> ?newpm . }
+	GRAPH ?g
+		{ ?gb <http://example.com/terms#prime_minister> ?oldpm . }
+	FILTER (?newpm != ?oldpm)
+}
+SPARQL
+my $model = RDF::Trine::Model->new($store);
+print $sparql->execute($model);

File lib/RDF/TrineX/Store/Tardis.pm

+package RDF::TrineX::Store::Tardis;
+
+use 5.010;
+use strict;
+
+use Carp qw[carp];
+use DateTime;
+use DateTime::Format::W3CDTF;
+use DBI;
+use RDF::Trine;
+use RDF::TrineX::Store::Tardis::SQLite;
+use RDF::TrineX::Store::Tardis::mysql;
+use RDF::TrineX::Store::Tardis::Pg;
+use Scalar::Util qw[blessed];
+
+use base qw[RDF::Trine::Store::DBI];
+
+our $VERSION = '0.000_01';
+
+sub new
+{
+	my $class = shift;
+	my $name  = shift || 'model';
+	my ($dbh, %args);
+
+	if (scalar(@_) == 0)
+	{
+		$dbh = DBI->connect('dbi:SQLite:dbname=:memory:');
+	}
+	elsif (blessed($_[0]) and $_[0]->isa('DBI::db'))
+	{
+		($dbh, %args) = @_;
+	}
+	else
+	{
+		(my $dsn, my $user, my $pass, %args) = @_;
+		$dbh = DBI->connect($dsn, $user, $pass);
+	}
+	
+	if (blessed($dbh) and $dbh->isa('DBI::db'))
+	{
+		if (lc $class eq lc __PACKAGE__)
+		{
+			given ($dbh->get_info(17))
+			{
+				when (/SQLite/i)      {  $class .= "::SQLite";  }
+				when (/MySQL/i)       {  $class .= "::mysql";  }
+				when (/PostgreSQL/i)  {  $class .= "::Pg";  }
+			}
+		}
+	}
+
+	my $self = bless {
+		model_name  => $name,
+		dbh         => $dbh,
+		statements_table_prefix => 'Statements',
+		%args,
+		}, $class;
+	$self->init;
+	return $self;
+}
+
+sub _begin_bulk_ops
+{
+	my ($self) = @_;
+	$self->{_tardis}->{bulk_ops} = DateTime->now;
+	$self->SUPER::_begin_bulk_ops();
+}
+
+sub _end_bulk_ops
+{
+	my ($self) = @_;
+	delete $self->{_tardis}->{bulk_ops};
+	$self->SUPER::_begin_bulk_ops();
+}
+
+sub add_statement
+{
+	my ($self, $stmt, $ctx) = @_;
+	
+	carp "Tardis store does not support true quads\n"
+		if $stmt->isa('RDF::Trine::Statement::Quad')
+		|| blessed($ctx);
+		
+	$self->tardis_add_statement(
+		statement($stmt->subject, $stmt->predicate, $stmt->object),
+		$self->{_tardis}->{bulk_ops} || DateTime->now);
+}
+
+sub remove_statement
+{
+	my ($self, $stmt, $ctx) = @_;
+	
+	carp "Tardis store does not support true quads\n"
+		if $stmt->isa('RDF::Trine::Statement::Quad')
+		|| blessed($ctx);
+		
+	$self->tardis_remove_statement(
+		statement($stmt->subject, $stmt->predicate, $stmt->object),
+		$self->{_tardis}->{bulk_ops} || DateTime->now);
+}
+
+sub remove_statements
+{
+	my ($self, $subj, $pred, $obj, $context) = @_;
+	my $dbh    = $self->dbh;
+	my $stable = $self->statements_table;
+	
+	carp "Tardis store does not support true quads\n"
+		if blessed($context);
+	
+	my (@where, @bind);
+	my @keys = qw(Subject Predicate Object);
+	
+	foreach my $node ($subj, $pred, $obj)
+	{
+		my $key = shift @keys;
+		if (defined $node)
+		{
+			push @bind, $node;
+			push @where, sprintf('%s = ?', $key);
+		}
+	}
+	
+	my $when   = $self->tardis_sql_datetime($self->{_tardis}->{bulk_ops} || DateTime->now);
+	my $where  = join ' AND ', @where;
+	my $sth    = $dbh->prepare(<<SQL);
+UPDATE $stable
+SET Removed = $when
+WHERE $where
+AND Removed IS NULL
+AND Added <= $when
+SQL
+	$sth->execute(map { $self->_mysql_node_hash($_) } @bind);
+}
+
+sub tardis_add_statement
+{
+	my ($self, $stmt, $context) = @_;
+	my $stable = $self->statements_table;
+	
+	if ($stmt->isa('RDF::Trine::Statement::Quad'))
+	{
+		throw RDF::Trine::Error::MethodInvocationError -text => "add_statement cannot be called with both a quad and a context"
+			if (blessed($context));
+		
+		$context = $stmt->context;
+	}
+	unless ($context->isa('DateTime'))
+	{
+		$context = $self->tardis_parse_iri_to_datetime($context);
+	}
+	$context = DateTime->now unless blessed($context);
+	
+	my @nodes = ($stmt->subject, $stmt->predicate, $stmt->object, $self->tardis_make_iri($context));
+	my @hashes = map {
+		$self->_add_node($_);
+		$self->_mysql_node_hash($_);
+		} @nodes;
+	
+	my $when = $self->tardis_sql_datetime($context);
+	my $sql = <<SQL;
+SELECT 1
+FROM $stable
+WHERE Subject = ?
+AND Predicate = ?
+AND Object = ?
+AND Added <= $when
+AND (Removed >= $when OR Removed IS NULL)
+SQL
+
+	my $sth = $self->dbh->prepare($sql);
+	$sth->execute(@hashes[0..2]);
+	unless ($sth->fetch)
+	{
+		my $sth = $self->dbh->prepare("INSERT INTO $stable VALUES (?,?,?,?,$when,NULL)");
+		$sth->execute(@hashes);
+	}
+}
+
+sub tardis_remove_statement
+{
+	my ($self, $stmt, $context) = @_;
+	my $stable = $self->statements_table;
+	
+	if ($stmt->isa('RDF::Trine::Statement::Quad'))
+	{
+		throw RDF::Trine::Error::MethodInvocationError -text => "add_statement cannot be called with both a quad and a context"
+			if (blessed($context));
+		
+		$context = $stmt->context;
+	}
+	
+	unless ($context->isa('DateTime'))
+	{
+		$context = $self->tardis_parse_iri_to_datetime($context);
+	}
+	$context = DateTime->now unless blessed($context);
+	my $when = $self->tardis_sql_datetime($context);
+	
+	my @nodes = ($stmt->subject, $stmt->predicate, $stmt->object);
+	my @hashes = map {
+		$self->_mysql_node_hash($_);
+		} @nodes;
+
+	my $sql = <<SQL;
+UPDATE $stable
+SET Removed = $when
+WHERE Subject = ?
+AND Predicate = ?
+AND Object = ?
+AND Removed IS NULL
+AND Added <= $when
+SQL
+
+	my $sth = $self->dbh->prepare($sql);
+	$sth->execute(@hashes);
+}
+
+sub get_statements
+{
+	my $self = shift;
+	if (scalar @_ >= 4)
+	{
+		my @nodes = @_[0..3];
+		if (blessed($nodes[3]) and $nodes[3]->isa('DateTime'))
+		{
+			$nodes[3] = $self->tardis_make_iri($nodes[3]);
+		}
+		return $self->SUPER::get_statements(@nodes);
+	}
+	
+	# get_statements called without a context only returns current triples.
+	my $when = $self->tardis_make_iri($self->{_tardis}->{bulk_ops} || DateTime->now);
+	my $iter = $self->SUPER::get_statements($_[0]||undef, $_[1]||undef, $_[2]||undef, $when);
+	my $sub  = sub {
+		my $st = $iter->next;
+		return unless $st;
+		return RDF::Trine::Statement->new($st->subject, $st->predicate, $st->object);
+		};
+	return RDF::Trine::Iterator::Graph->new($sub);
+}
+
+sub tardis_make_iri
+{
+	my ($self, $dt) = @_;
+	return RDF::Trine::Node::Resource->new('tempus:now') if !blessed($dt);
+	RDF::Trine::Node::Resource->new(sprintf('tempus:%s', $dt->strftime('%FT%TZ')));
+}
+
+sub tardis_parse_iri
+{
+	my ($self, $iri) = @_;
+	
+	if (blessed($iri) and $iri->is_resource and $iri->uri =~ /^tempus:(.+)$/i)
+	{
+		return $1;
+	}
+
+	if (!ref $iri and $iri =~ /^tempus:(.+)$/i)
+	{
+		return $1;
+	}
+
+	return;
+}
+
+sub tardis_parse_iri_to_datetime
+{
+	my ($self, $iri) = @_;
+	my $dt = $self->tardis_parse_iri($iri);
+	
+	return DateTime->now->set_time_zone('UTC') if $dt =~ /^now$/i;
+	
+	my $parser = DateTime::Format::W3CDTF->new;
+	return $parser->parse_datetime($dt)->set_time_zone('UTC');
+}
+
+sub tardis_range
+{
+	my ($self, $tempus) = @_;
+	$tempus = $self->parse_graph_uri($tempus) if ref $tempus;
+	
+	return (DateTime->now->set_time_zone('UTC'), DateTime->now->set_time_zone('UTC')) if $tempus =~ /^now$/i;
+	
+	my ($start, $end) = split m'/', $tempus;
+	$start = '1970-01-01T00:00:00' unless $start;
+	$end   = $start unless $end;
+	
+	my $parser = DateTime::Format::W3CDTF->new;
+	my ($start_dt, $end_dt) = map { $parser->parse_datetime($_) } ($start, $end);
+	
+	$start_dt->set(hour => 0, minute => 0, second => 0) unless $start =~ /t/i;
+	$end_dt->set(hour => 0, minute => 0, second => 0)->add(days => 1) unless $end =~ /t/i;
+	
+	return ($start_dt, $end_dt);
+}
+
+sub tardis_sql_datetime
+{
+	my ($self, $dt) = @_;
+	$dt->set_time_zone('UTC')->strftime("'%F %T'");
+}
+
+sub _sql_for_statement
+{
+	my ($self, $triple, $ctx, $context, %args) = @_;
+	
+	my $tempus = $self->tardis_parse_iri($triple->isa('RDF::Trine::Statement::Quad') ? $triple->context : $ctx);
+	if ($tempus)
+	{
+		my $hash = $self->_mysql_node_hash($triple->isa('RDF::Trine::Statement::Quad') ? $triple->context : $ctx);
+		$context->{_tardis}{$hash} = $tempus;
+	}
+	
+	return $self->SUPER::_sql_for_statement($triple, $ctx, $context, %args);
+}
+
+*_sql_for_triple = \&_sql_for_statement;
+*_sql_for_quad   = \&_sql_for_statement;
+
+sub _sql_from_context
+{
+	my ($self, $context, @args) = @_;
+	do {
+		if ($context->{_tardis})
+		{
+			$context->{where_clauses} = [ map {
+					my $x = $_;
+					KEY: foreach my $key (keys %{ $context->{_tardis} })
+					{
+						if ($x =~ /^(.+)\.context\s*=\s*(\d+)$/i)
+						{
+							my ($table, $value) = ($1, $2);
+							next KEY unless $value==$key;
+							
+							my ($start, $end) = map
+								{ $self->tardis_sql_datetime($_) }
+								$self->tardis_range($context->{_tardis}{$key});
+							$x = "(${table}.added <= $start AND (${table}.removed >= $end OR ${table}.removed IS NULL))";
+							
+							last KEY;
+						}
+					}
+					$x;
+				} @{ $context->{where_clauses} } ];
+		}
+	};
+	my $sql = $self->SUPER::_sql_from_context($context, @args);
+	# print "$sql\n";
+	return $sql;
+}
+
+sub init
+{
+	my ($self) = @_;
+	my $dbh  = $self->dbh;
+	my $name = $self->model_name;
+	my $id   = RDF::Trine::Store::DBI::_mysql_hash( $name );
+	
+	unless ($self->_table_exists("Statements${id}"))
+	{
+		$dbh->do( <<END ) || do { return undef };
+			CREATE TABLE Statements${id} (
+				Subject NUMERIC(20) NOT NULL,
+				Predicate NUMERIC(20) NOT NULL,
+				Object NUMERIC(20) NOT NULL,
+				Context NUMERIC(20) NOT NULL DEFAULT 0,
+				Added TIMESTAMP NOT NULL DEFAULT(CURRENT_TIMESTAMP),
+				Removed TIMESTAMP,
+				PRIMARY KEY( Subject, Predicate, Object, Context ),
+				CHECK (Added <= Removed OR Removed IS NULL)
+			);
+END
+	}
+	
+	return $self->SUPER::init;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+RDF::TrineX::Store::Tardis - time travelling triple store
+
+=head1 SYNOPSIS
+
+ use DateTime;
+ use RDF::Trine qw[iri statement];
+ use RDF::Trine::Store::Tardis;
+ 
+ my $S = iri('http://example.com/s');
+ my $P = iri('http://example.com/p');
+ my $O = iri('http://example.com/o');
+ 
+ my $store = RDF::Trine::Store::Tardis->new;
+ 
+ $store->add(statement($S, $P, $O));
+ sleep 5;
+ 
+ $store->remove(statement($S, $P, $O));
+ sleep 1;
+ 
+ my $then     = DateTime->now->subtract(seconds => 3);
+ my $then_iri = $store->tardis_make_iri($then);
+ my $results  = $store->get_statements(undef, undef, undef, $then_iri);
+ while (my $result = $results->next)
+ {
+   print $result->as_string . "\n";
+ }
+
+=head1 DESCRIPTION
+
+Tempus is a triple store (not a quad store!) that keeps track of when triples
+were added and removed from the graph.
+
+=head2 Constructor
+
+=over 
+
+=item C<< new([$name]) >>
+
+Constructs a temporary store.
+
+=item C<< new($name, $dbh) >>
+
+Uses an existing DBI database connection.
+
+=item C<< new($name, $dsn, $username, $password) >>
+
+Opens a DBI database connection using supplied DSN, username and password.
+
+=back
+
+Note that while it's not especially feasible to share the same database and
+model name between Tardis and L<RDF::Trine::Store::DBI>, the latter should be
+able to safely *read* a Tardis store.
+
+=head2 Methods
+
+Tempus inherits from L<RDF::Trine::Store> so provides all the methods of an
+RDF::Trine::Store. The following additional methods and differences are
+noteworthy:
+
+=over
+
+=item C<< add_statement($statement) >>
+
+=item C<< remove_statement($statement) >>
+
+=item C<< remove_statements($subj, $pred, $obj) >>
+
+Can only be handle a triple, not a quad. If passed a quad, a warning is given
+and the context is ignored.
+
+=item C<< get_statements($subj, $pred, $obj) >>
+
+Ignores historic and futuristic triples, only returning triples that are valid
+at the current time.
+
+=item C<< get_statements($subj, $pred, $obj, $context) >>
+
+C<$context> must be an IRI if the form E<lt>tempus:2003-02-01T12:00:00ZE<gt>
+or a L<DateTime> object. Returns triples valid at that time.
+
+=item C<< get_statements($subj, $pred, $obj, undef) >>
+
+Returns current, historic and future triples.
+
+=item C<< tardis_add_statement($triple, $datetime) >>
+
+=item C<< tardis_remove_statement($triple, $datetime) >>
+
+Add or remove a statement at a time other than now. C<$datetime> can be a
+special tempus URI, or a C<DateTime> object.
+
+=item C<< tardis_add_statement($quad) >>
+
+=item C<< tardis_remove_statement($quad) >>
+
+Quads are supported for these methods, provided that the context is a special
+tempus URI.
+
+=item C<< tardis_make_iri($datetime) >>
+
+Given a C<DateTime> object, returns an L<RDF::Trine::Node::Resource> for a
+special tempus URI representing that time.
+
+=item C<< tardis_parse_iri_to_datetime($resource) >>
+
+Does the inverse of C<tardis_make_iri>.
+
+=back
+
+=head2 Tempus URIs
+
+Here is the EBNF notation for tempus URIs:
+
+ tempusURI = 'tempus:', ('now' | date | dateTime);
+ dateTime  = date, 'T', time;
+ date      = year, '-', month, '-', day;
+ time      = hour, ':', minute, [':', second,] [timezone];
+ year      = digit, digit, digit, digit;
+ month     = digit, digit;
+ day       = digit, digit;
+ hour      = digit, digit;
+ minute    = digit, digit;
+ second    = digit, digit;
+ timezone  = 'Z' | numTZ
+ numTZ     = ( '+' | '-' ), digit, digit, digit, digit;
+ digit     = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
+
+=head1 BUGS
+
+I imagine there are many.
+
+Please report any bugs to L<http://rt.cpan.org/>.
+
+=head1 SEE ALSO
+
+L<RDF::Trine>,
+L<DateTime>.
+
+L<RDF::TrineX::Store::Tardis::SQLite>,
+L<RDF::TrineX::Store::Tardis::mysql>,
+L<RDF::TrineX::Store::Tardis::Pg>.
+
+L<http://www.perlrdf.org/>.
+
+L<http://en.wikipedia.org/wiki/TARDIS>.
+
+=head1 AUTHOR
+
+Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+Copyright 2011 Toby Inkster
+
+This library is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut

File lib/RDF/TrineX/Store/Tardis/Pg.pm

+package RDF::TrineX::Store::Tardis::Pg;
+
+use 5.010;
+use strict;
+
+# Diamond inheritance from RDF::Trine::Store::DBI.
+use mro 'c3';
+use base qw[RDF::TrineX::Store::Tardis RDF::Trine::Store::DBI::Pg];
+
+our $VERSION = '0.000_01';
+
+1;
+
+__END__
+
+=head1 NAME
+
+RDF::TrineX::Store::Tardis::Pg - power your RDF Tardis with PostgreSQL
+
+=head1 SEE ALSO
+
+L<RDF::Trine::Store::DBI::Pg>,
+L<RDF::TrineX::Store::Tardis>.
+L<http://www.perlrdf.org/>.
+
+=head1 AUTHOR
+
+Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+Copyright 2011 Toby Inkster
+
+This library is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut

File lib/RDF/TrineX/Store/Tardis/SQLite.pm

+package RDF::TrineX::Store::Tardis::SQLite;
+
+use 5.010;
+use strict;
+
+# Diamond inheritance from RDF::Trine::Store::DBI.
+use mro 'c3';
+use base qw[RDF::TrineX::Store::Tardis RDF::Trine::Store::DBI::SQLite];
+
+our $VERSION = '0.000_01';
+
+sub init
+{
+	my ($self) = @_;
+	my $dbh  = $self->dbh;
+	my $name = $self->model_name;
+	my $id   = RDF::Trine::Store::DBI::_mysql_hash( $name );
+	
+	unless ($self->_table_exists("Statements${id}"))
+	{
+		$dbh->do( <<END ) || do { warn $dbh->errstr; return undef };
+			CREATE TABLE Statements${id} (
+				Subject NUMERIC(20) NOT NULL,
+				Predicate NUMERIC(20) NOT NULL,
+				Object NUMERIC(20) NOT NULL,
+				Context NUMERIC(20) NOT NULL DEFAULT 0,
+				Added TEXT NOT NULL DEFAULT(CURRENT_TIMESTAMP),
+				Removed TEXT,
+				PRIMARY KEY( Subject, Predicate, Object, Context ),
+				CHECK (Added <= Removed OR Removed IS NULL)
+			);
+END
+		$dbh->do( "CREATE INDEX idx_${name}_spog ON Statements${id} (Subject,Predicate,Object,Context);" ) || do { $dbh->rollback; return undef };
+		$dbh->do( "CREATE INDEX idx_${name}_pogs ON Statements${id} (Predicate,Object,Context,Subject);" ) || do { $dbh->rollback; return undef };
+		$dbh->do( "CREATE INDEX idx_${name}_opcs ON Statements${id} (Object,Predicate,Context,Subject);" ) || do { $dbh->rollback; return undef };
+		$dbh->do( "CREATE INDEX idx_${name}_cpos ON Statements${id} (Context,Predicate,Object,Subject);" ) || do { $dbh->rollback; return undef };
+	}
+
+	return RDF::Trine::Store::DBI::SQLite::init($self);
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+RDF::TrineX::Store::Tardis::SQLite - power your Tardis with SQLite
+
+=head1 SEE ALSO
+
+L<RDF::Trine::Store::DBI::SQLite>,
+L<RDF::TrineX::Store::Tardis>.
+L<http://www.perlrdf.org/>.
+
+=head1 AUTHOR
+
+Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+Copyright 2011 Toby Inkster
+
+This library is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut

File lib/RDF/TrineX/Store/Tardis/mysql.pm

+package RDF::TrineX::Store::Tardis::mysql;
+
+use 5.010;
+use strict;
+
+# Diamond inheritance from RDF::Trine::Store::DBI.
+use mro 'c3';
+use base qw[RDF::TrineX::Store::Tardis RDF::Trine::Store::DBI::mysql];
+
+our $VERSION = '0.000_01';
+
+1;
+
+__END__
+
+=head1 NAME
+
+RDF::TrineX::Store::Tardis::MySQL - power your RDF Tardis with MySQL
+
+=head1 SEE ALSO
+
+L<RDF::Trine::Store::DBI::mysql>,
+L<RDF::TrineX::Store::Tardis>.
+L<http://www.perlrdf.org/>.
+
+=head1 AUTHOR
+
+Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
+
+=head1 COPYRIGHT
+
+Copyright 2011 Toby Inkster
+
+This library is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+use Test::More tests => 1;
+BEGIN { use_ok('RDF::Trine::Store::Tardis') };
+