diametric 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +22 -0
- data/Gemfile +14 -0
- data/Guardfile +8 -0
- data/Jarfile +6 -0
- data/Jarfile.lock +126 -0
- data/LICENSE.txt +22 -0
- data/README.md +215 -0
- data/Rakefile +17 -0
- data/TODO.org +12 -0
- data/diametric.gemspec +31 -0
- data/lib/diametric.rb +9 -0
- data/lib/diametric/entity.rb +339 -0
- data/lib/diametric/persistence/common.rb +29 -0
- data/lib/diametric/persistence/peer.rb +107 -0
- data/lib/diametric/persistence/rest.rb +88 -0
- data/lib/diametric/query.rb +157 -0
- data/lib/diametric/version.rb +3 -0
- data/lib/jrclj.rb +63 -0
- data/spec/diametric/entity_spec.rb +128 -0
- data/spec/diametric/persistence/peer_spec.rb +36 -0
- data/spec/diametric/persistence/rest_spec.rb +30 -0
- data/spec/diametric/query_spec.rb +63 -0
- data/spec/integration_spec.rb +67 -0
- data/spec/spec_helper.rb +53 -0
- data/spec/support/persistence_examples.rb +68 -0
- metadata +164 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'diametric'
|
2
|
+
require 'diametric/persistence/common'
|
3
|
+
require 'datomic/client'
|
4
|
+
|
5
|
+
module Diametric
|
6
|
+
module Persistence
|
7
|
+
module REST
|
8
|
+
@connection = nil
|
9
|
+
@persisted_classes = Set.new
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.send(:include, Diametric::Persistence::Common)
|
13
|
+
base.send(:extend, ClassMethods)
|
14
|
+
@persisted_classes.add(base)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.connection
|
18
|
+
@connection
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.create_schemas
|
22
|
+
@persisted_classes.each do |klass|
|
23
|
+
klass.create_schema
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
def connect(uri, dbalias, database)
|
29
|
+
@uri = uri
|
30
|
+
@dbalias = dbalias
|
31
|
+
@database = database
|
32
|
+
|
33
|
+
@connection = Datomic::Client.new(uri, dbalias)
|
34
|
+
@connection.create_database(database)
|
35
|
+
end
|
36
|
+
|
37
|
+
def connection
|
38
|
+
@connection || Diametric::Persistence::REST.connection
|
39
|
+
end
|
40
|
+
|
41
|
+
def database
|
42
|
+
@database || Diametric::Persistence::REST.database
|
43
|
+
end
|
44
|
+
|
45
|
+
def transact(data)
|
46
|
+
connection.transact(database, data)
|
47
|
+
end
|
48
|
+
|
49
|
+
def get(dbid)
|
50
|
+
res = connection.entity(database, dbid)
|
51
|
+
|
52
|
+
# TODO tighten regex to only allow fields with the model name
|
53
|
+
attrs = res.data.map { |attr_symbol, value|
|
54
|
+
attr = attr_symbol.to_s.gsub(%r"^\w+/", '')
|
55
|
+
[attr, value]
|
56
|
+
}
|
57
|
+
|
58
|
+
entity = self.new(Hash[*attrs.flatten])
|
59
|
+
entity.dbid = dbid
|
60
|
+
entity
|
61
|
+
end
|
62
|
+
|
63
|
+
def q(query, args)
|
64
|
+
args.unshift(connection.db_alias(database))
|
65
|
+
res = connection.query(query, args)
|
66
|
+
res.data
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
extend ClassMethods
|
71
|
+
|
72
|
+
def save
|
73
|
+
return false unless valid?
|
74
|
+
return true unless changed?
|
75
|
+
|
76
|
+
res = self.class.transact(tx_data)
|
77
|
+
if dbid.nil?
|
78
|
+
self.dbid = res.data[:tempids].values.first
|
79
|
+
end
|
80
|
+
|
81
|
+
@previously_changed = changes
|
82
|
+
@changed_attributes.clear
|
83
|
+
|
84
|
+
res
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'diametric'
|
2
|
+
|
3
|
+
module Diametric
|
4
|
+
# +Query+ objects are used to generate Datomic queries, whether to
|
5
|
+
# send via an external client or via the persistence API. The two
|
6
|
+
# methods used to generate a query are +.where+ and +.filter+, both
|
7
|
+
# of which are chainable. To get the query data and arguments for a
|
8
|
+
# +Query+, use the +data+ method.
|
9
|
+
#
|
10
|
+
# If you are using a persistence API, you can ask +Query+ to get the
|
11
|
+
# results of a Datomic query. +Diametric::Query+ is an
|
12
|
+
# +Enumerable+. To get the results of a query, use +Enumerable+
|
13
|
+
# methods such as +.each+ or +.first+. +Query+ also provides a
|
14
|
+
# +.all+ method to run the query and get the results.
|
15
|
+
class Query
|
16
|
+
include Enumerable
|
17
|
+
|
18
|
+
attr_reader :conditions, :filters, :model
|
19
|
+
|
20
|
+
# Create a new Datomic query.
|
21
|
+
#
|
22
|
+
# @param model [Entity] This model must include +Datomic::Entity+. Including
|
23
|
+
# a persistence module is optional.
|
24
|
+
def initialize(model)
|
25
|
+
@model = model
|
26
|
+
@conditions = {}
|
27
|
+
@filters = []
|
28
|
+
end
|
29
|
+
|
30
|
+
# Add conditions to your Datomic query. Conditions check for equality
|
31
|
+
# against entity attributes. In addition, you can add conditions for
|
32
|
+
# use as variables in filters.
|
33
|
+
#
|
34
|
+
# @example Looking for mice named Wilbur.
|
35
|
+
# Query.new(Mouse).conditions(:name => "Wilbur")
|
36
|
+
#
|
37
|
+
# @param conditions [Hash] Datomic variables and values.
|
38
|
+
# @return [Query]
|
39
|
+
def where(conditions)
|
40
|
+
query = self.dup
|
41
|
+
query.conditions = query.conditions.merge(conditions)
|
42
|
+
query
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add a filter to your Datomic query. Filters are known as expression clause
|
46
|
+
# predicates in the {Datomic query documentation}[http://docs.datomic.com/query.html].
|
47
|
+
#
|
48
|
+
# A filter can be in one of two forms. In the first, you pass a
|
49
|
+
# series of arguments. Any Ruby symbol given in this form will be
|
50
|
+
# converted to a EDN symbol. If the symbol is the same as one of
|
51
|
+
# the queried model's attributes or as a key passed to +where+, it
|
52
|
+
# will be prefixed with a +?+ so that it becomes a Datalog
|
53
|
+
# variable. In the second form, you pass
|
54
|
+
# {EDN}[https://github.com/relevance/edn-ruby] representing a
|
55
|
+
# Datomic predicate to +filter+. No conversion is done on this
|
56
|
+
# filter and it must be an EDN list.
|
57
|
+
#
|
58
|
+
# @example Passing arguments to be converted.
|
59
|
+
# query.filter(:>, :age, 21)
|
60
|
+
#
|
61
|
+
# @example Passing EDN, which will not be converted.
|
62
|
+
# query.filter(EDN::Type::List.new(EDN::Type::Symbol(">"),
|
63
|
+
# EDN::Type::Symbol("?age"),
|
64
|
+
# 21))
|
65
|
+
# # or, more simply
|
66
|
+
# query.filter(~[~">", ~"?age", 21])
|
67
|
+
#
|
68
|
+
# @param filter [Array] Either one +EDN::Type::List+ or a number of arguments
|
69
|
+
# that will be converted into a Datalog query.
|
70
|
+
# @return [Query]
|
71
|
+
def filter(*filter)
|
72
|
+
query = self.dup
|
73
|
+
|
74
|
+
if filter.first.is_a?(EDN::Type::List)
|
75
|
+
filter = filter.first
|
76
|
+
else
|
77
|
+
filter = filter.map { |e| convert_filter_element(e) }
|
78
|
+
filter = EDN::Type::List.new(*filter)
|
79
|
+
end
|
80
|
+
|
81
|
+
query.filters += [[filter]]
|
82
|
+
query
|
83
|
+
end
|
84
|
+
|
85
|
+
# Loop through the query results. In order to use +each+, your model *must*
|
86
|
+
# include a persistence API. At a minimum, it must have a +.q+ method that
|
87
|
+
# returns an +Enumerable+ object.
|
88
|
+
#
|
89
|
+
# @yield [Entity] An instance of the model passed to +Query+.
|
90
|
+
def each
|
91
|
+
# TODO check to see if the model has a `.q` method and give
|
92
|
+
# an appropriate error if not.
|
93
|
+
res = model.q(*data)
|
94
|
+
res.each do |entity|
|
95
|
+
# The map is for compatibility with Java peer persistence.
|
96
|
+
# TODO remove if possible
|
97
|
+
yield model.from_query(entity.map { |x| x })
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Return all query results.
|
102
|
+
#
|
103
|
+
# @return [Array<Entity>] Query results.
|
104
|
+
def all
|
105
|
+
map { |x| x }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Create a Datomic query from the conditions and filters passed to this
|
109
|
+
# +Query+ object.
|
110
|
+
#
|
111
|
+
# @return [Array(Array, Array)] The first element of the array returned
|
112
|
+
# is the Datomic query composed of Ruby data. The second element is
|
113
|
+
# the arguments that used with the query.
|
114
|
+
def data
|
115
|
+
vars = model.attributes.map { |attribute, _, _| ~"?#{attribute}" }
|
116
|
+
|
117
|
+
from = conditions.map { |k, _| ~"?#{k}" }
|
118
|
+
|
119
|
+
clauses = model.attributes.map { |attribute, _, _|
|
120
|
+
[~"?e", model.namespace(model.prefix, attribute), ~"?#{attribute}"]
|
121
|
+
}
|
122
|
+
clauses += filters
|
123
|
+
|
124
|
+
args = conditions.map { |_, v| v }
|
125
|
+
|
126
|
+
query = [
|
127
|
+
:find, ~"?e", *vars,
|
128
|
+
:in, ~"\$", *from,
|
129
|
+
:where, *clauses
|
130
|
+
]
|
131
|
+
|
132
|
+
[query, args]
|
133
|
+
end
|
134
|
+
|
135
|
+
protected
|
136
|
+
|
137
|
+
def conditions=(conditions)
|
138
|
+
@conditions = conditions
|
139
|
+
end
|
140
|
+
|
141
|
+
def filters=(filters)
|
142
|
+
@filters = filters
|
143
|
+
end
|
144
|
+
|
145
|
+
def convert_filter_element(element)
|
146
|
+
if element.is_a?(Symbol)
|
147
|
+
if model.attribute_names.include?(element) || @conditions.keys.include?(element)
|
148
|
+
EDN::Type::Symbol.new("?#{element}")
|
149
|
+
else
|
150
|
+
EDN::Type::Symbol.new(element.to_s)
|
151
|
+
end
|
152
|
+
else
|
153
|
+
element
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/lib/jrclj.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'java'
|
2
|
+
|
3
|
+
java_import "clojure.lang.RT"
|
4
|
+
|
5
|
+
class JRClj
|
6
|
+
def initialize *pkgs
|
7
|
+
@mappings = {}
|
8
|
+
@ns_map = RT.var "clojure.core", "ns-map"
|
9
|
+
@symbol = RT.var "clojure.core", "symbol"
|
10
|
+
@require = RT.var "clojure.core", "require"
|
11
|
+
_import "clojure.core"
|
12
|
+
pkgs.each do |pkg|
|
13
|
+
_import pkg
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def _import pkg_name, sym=nil, sym_alias=nil
|
18
|
+
@require.invoke @symbol.invoke(pkg_name)
|
19
|
+
if sym
|
20
|
+
sym_alias ||= sym
|
21
|
+
@mappings[sym_alias] = RT.var pkg_name, sym
|
22
|
+
return
|
23
|
+
end
|
24
|
+
pkg = @symbol.invoke pkg_name
|
25
|
+
@ns_map.invoke(pkg).each do |sym,var|
|
26
|
+
@mappings[sym.to_s] = var
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def _eval s
|
31
|
+
self.eval self.read_string(s)
|
32
|
+
end
|
33
|
+
|
34
|
+
def _invoke m, *args
|
35
|
+
fun = @mappings[m.to_s] || @mappings[m.to_s.gsub "_", "-"]
|
36
|
+
unless fun
|
37
|
+
raise "Error, no current binding for symbol=#{m}"
|
38
|
+
end
|
39
|
+
fun.invoke(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
def _alias new, old
|
43
|
+
@mappings[new] = @mappings[old]
|
44
|
+
end
|
45
|
+
|
46
|
+
def method_missing symbol, *args
|
47
|
+
_invoke symbol, *args
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.persistent_map entries=[]
|
51
|
+
Java::ClojureLang::PersistentArrayMap.new entries.to_java
|
52
|
+
end
|
53
|
+
|
54
|
+
def edn_convert(obj)
|
55
|
+
self.read_string(obj.to_edn)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Symbol
|
60
|
+
def to_clj
|
61
|
+
Java::ClojureLang::Keyword.intern(self.to_s)
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Diametric::Entity do
|
4
|
+
describe "in a class" do
|
5
|
+
subject { Person }
|
6
|
+
|
7
|
+
it { should respond_to(:attribute) }
|
8
|
+
it { should respond_to(:schema) }
|
9
|
+
it { should respond_to(:from_query) }
|
10
|
+
|
11
|
+
it "should generate a schema" do
|
12
|
+
Person.schema.should == [
|
13
|
+
{ :"db/id" => Person.send(:tempid, :"db.part/db"),
|
14
|
+
:"db/ident" => :"person/name",
|
15
|
+
:"db/valueType" => :"db.type/string",
|
16
|
+
:"db/cardinality" => :"db.cardinality/one",
|
17
|
+
:"db/index" => true,
|
18
|
+
:"db.install/_attribute" => :"db.part/db" },
|
19
|
+
{ :"db/id" => Person.send(:tempid, :"db.part/db"),
|
20
|
+
:"db/ident" => :"person/email",
|
21
|
+
:"db/valueType" => :"db.type/string",
|
22
|
+
:"db/cardinality" => :"db.cardinality/many",
|
23
|
+
:"db.install/_attribute" => :"db.part/db" },
|
24
|
+
{ :"db/id" => Person.send(:tempid, :"db.part/db"),
|
25
|
+
:"db/ident" => :"person/birthday",
|
26
|
+
:"db/valueType" => :"db.type/instant",
|
27
|
+
:"db/cardinality" => :"db.cardinality/one",
|
28
|
+
:"db.install/_attribute" => :"db.part/db" },
|
29
|
+
{ :"db/id" => Person.send(:tempid, :"db.part/db"),
|
30
|
+
:"db/ident" => :"person/awesome",
|
31
|
+
:"db/valueType" => :"db.type/boolean",
|
32
|
+
:"db/cardinality" => :"db.cardinality/one",
|
33
|
+
:"db/doc" => "Is this person awesome?",
|
34
|
+
:"db.install/_attribute" => :"db.part/db" },
|
35
|
+
{ :"db/id" => Person.send(:tempid, :"db.part/db"),
|
36
|
+
:"db/ident" => :"person/ssn",
|
37
|
+
:"db/valueType" => :"db.type/string",
|
38
|
+
:"db/cardinality" => :"db.cardinality/one",
|
39
|
+
:"db/unique" => :"db.unique/value",
|
40
|
+
:"db.install/_attribute" => :"db.part/db" },
|
41
|
+
{ :"db/id" => Person.send(:tempid, :"db.part/db"),
|
42
|
+
:"db/ident" => :"person/secret_name",
|
43
|
+
:"db/valueType" => :"db.type/string",
|
44
|
+
:"db/cardinality" => :"db.cardinality/one",
|
45
|
+
:"db/unique" => :"db.unique/identity",
|
46
|
+
:"db.install/_attribute" => :"db.part/db" },
|
47
|
+
{ :"db/id" => Person.send(:tempid, :"db.part/db"),
|
48
|
+
:"db/ident" => :"person/bio",
|
49
|
+
:"db/valueType" => :"db.type/string",
|
50
|
+
:"db/cardinality" => :"db.cardinality/one",
|
51
|
+
:"db/fulltext" => true,
|
52
|
+
:"db.install/_attribute" => :"db.part/db" }
|
53
|
+
]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "in an instance" do
|
58
|
+
subject { Person.new }
|
59
|
+
let(:model) { Person.new }
|
60
|
+
|
61
|
+
it_should_behave_like "ActiveModel"
|
62
|
+
|
63
|
+
it { should respond_to(:tx_data) }
|
64
|
+
|
65
|
+
it "should handle attributes correctly" do
|
66
|
+
subject.name.should be_nil
|
67
|
+
subject.name = "Clinton"
|
68
|
+
subject.name.should == "Clinton"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe ".new" do
|
73
|
+
it "should work without arguments" do
|
74
|
+
Person.new.should be_a(Person)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should assign attributes based off argument keys" do
|
78
|
+
person = Person.new(:name => "Dashiell D", :secret_name => "Monito")
|
79
|
+
person.name.should == "Dashiell D"
|
80
|
+
person.secret_name.should == "Monito"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe ".from_query" do
|
85
|
+
it "should assign dbid and attributes" do
|
86
|
+
goat = Goat.from_query([1, "Beans", DateTime.parse("1976/9/4")])
|
87
|
+
goat.dbid.should == 1
|
88
|
+
goat.name.should == "Beans"
|
89
|
+
goat.birthday.should == DateTime.parse("1976/9/4")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "#tx_data" do
|
94
|
+
let(:goat) { Goat.new(:name => "Beans", :birthday => Date.parse("2002-04-15"))}
|
95
|
+
|
96
|
+
describe "without a dbid" do
|
97
|
+
it "should generate a transaction with a new tempid" do
|
98
|
+
# Equivalence is currently wrong on EDN tagged values.
|
99
|
+
tx = goat.tx_data.first
|
100
|
+
tx.keys.should == [:"db/id", :"goat/name", :"goat/birthday"]
|
101
|
+
tx[:"db/id"].to_edn.should match(%r"#db/id \[:db.part/user \-\d+\]")
|
102
|
+
tx[:"goat/name"].should == "Beans"
|
103
|
+
tx[:"goat/birthday"].should == goat.birthday
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "with a dbid" do
|
108
|
+
it "should generate a transaction with the dbid" do
|
109
|
+
goat.dbid = 1
|
110
|
+
goat.tx_data.should == [
|
111
|
+
{ :"db/id" => 1,
|
112
|
+
:"goat/name" => "Beans",
|
113
|
+
:"goat/birthday" => goat.birthday
|
114
|
+
}
|
115
|
+
]
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should generate a transaction with only specified attributes" do
|
119
|
+
goat.dbid = 1
|
120
|
+
goat.tx_data(:name).should == [
|
121
|
+
{ :"db/id" => 1,
|
122
|
+
:"goat/name" => "Beans"
|
123
|
+
}
|
124
|
+
]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'diametric/persistence/peer' if RUBY_ENGINE == 'jruby'
|
3
|
+
|
4
|
+
# Prevent CRuby from blowing up
|
5
|
+
module Diametric
|
6
|
+
module Persistence
|
7
|
+
module Peer
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe Diametric::Persistence::Peer, :jruby do
|
13
|
+
class Rat
|
14
|
+
include Diametric::Entity
|
15
|
+
include Diametric::Persistence::Peer
|
16
|
+
|
17
|
+
attribute :name, String, :index => true
|
18
|
+
attribute :age, Integer
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:db_uri) { 'datomic:mem://hello' }
|
22
|
+
|
23
|
+
it "can connect to a Datomic database" do
|
24
|
+
subject.connect(db_uri)
|
25
|
+
subject.connection.should be_a(Java::DatomicPeer::LocalConnection)
|
26
|
+
end
|
27
|
+
|
28
|
+
it_behaves_like "persistence API" do
|
29
|
+
let(:model_class) { Rat }
|
30
|
+
|
31
|
+
before(:all) do
|
32
|
+
subject.connect(db_uri)
|
33
|
+
Diametric::Persistence::Peer.create_schemas
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|