dalton 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.jrubyrc +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +8 -0
- data/Jarfile +7 -0
- data/README.md +35 -0
- data/Rakefile +7 -0
- data/dalton.gemspec +23 -0
- data/epl-v10.html +261 -0
- data/examples/posts_controller.rb +48 -0
- data/lib/dalton.rb +37 -0
- data/lib/dalton/attribute.rb +16 -0
- data/lib/dalton/connection.rb +104 -0
- data/lib/dalton/database.rb +48 -0
- data/lib/dalton/datomization.rb +18 -0
- data/lib/dalton/entity.rb +75 -0
- data/lib/dalton/exception.rb +68 -0
- data/lib/dalton/transaction_result.rb +33 -0
- data/lib/dalton/translation.rb +65 -0
- data/lib/dalton/undatomization.rb +11 -0
- data/lib/dalton/utility.rb +91 -0
- data/lib/dalton/version.rb +3 -0
- data/spec/dalton/connection_spec.rb +133 -0
- data/spec/dalton/database_spec.rb +85 -0
- data/spec/dalton/datomization_spec.rb +94 -0
- data/spec/dalton/entity_spec.rb +37 -0
- data/spec/dalton/translation_spec.rb +126 -0
- data/spec/dalton/utility_spec.rb +37 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/clojure_equal_matcher.rb +52 -0
- data/spec/support/datomic_context.rb +19 -0
- metadata +101 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
module Dalton
|
2
|
+
class Attribute
|
3
|
+
def initialize(datomic_attribute)
|
4
|
+
@datomic_attribute = datomic_attribute
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(name, *args, &block)
|
8
|
+
if @datomic_attribute.respond_to?(name)
|
9
|
+
Translation.from_clj(@datomic_attribute.send(name, *args, &block))
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
java_import "clojure.lang.Keyword"
|
2
|
+
java_import "datomic.Peer"
|
3
|
+
|
4
|
+
module Dalton
|
5
|
+
class Connection
|
6
|
+
|
7
|
+
include Dalton::Datomization
|
8
|
+
|
9
|
+
def initialize(uri)
|
10
|
+
@uri = uri
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.connect(uri)
|
14
|
+
Peer.createDatabase(uri)
|
15
|
+
database = new(uri)
|
16
|
+
database.connect
|
17
|
+
return database
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :uri, :datomic_connection, :db
|
21
|
+
|
22
|
+
def create
|
23
|
+
Peer.createDatabase(uri) or
|
24
|
+
raise DatomicError, "Unable to create database at \"#{uri}\"."
|
25
|
+
end
|
26
|
+
|
27
|
+
def destroy
|
28
|
+
Peer.deleteDatabase(uri) or
|
29
|
+
raise DatomicError, "Unable to destroy database at \"#{uri}\"."
|
30
|
+
end
|
31
|
+
|
32
|
+
def connect
|
33
|
+
@datomic_connection = Peer.connect(uri) or
|
34
|
+
raise DatomicError, "Unable to connect to database at \"#{uri}\"."
|
35
|
+
refresh
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def db=(new_db)
|
40
|
+
@db = new_db.is_a?(Database) ? new_db : Database.new(new_db)
|
41
|
+
end
|
42
|
+
|
43
|
+
def refresh
|
44
|
+
self.db = @datomic_connection.db
|
45
|
+
db
|
46
|
+
end
|
47
|
+
|
48
|
+
def transact(datoms)
|
49
|
+
data = self.class.convert_datoms(datoms)
|
50
|
+
# STDERR.puts "data=#{data.to_edn}"
|
51
|
+
result = TransactionResult.new(@datomic_connection.transact(data).get)
|
52
|
+
self.db = result.db_after
|
53
|
+
Translation.from_clj(result)
|
54
|
+
rescue Java::JavaUtilConcurrent::ExecutionException => e
|
55
|
+
cause = e.getCause
|
56
|
+
if cause.respond_to?(:data)
|
57
|
+
err_data = Translation.from_clj(cause.data)
|
58
|
+
case err_data[:'db/error']
|
59
|
+
when :'db.error/unique-conflict'
|
60
|
+
raise UniqueConflict.parse(cause.getMessage)
|
61
|
+
when :'db.error/wrong-type-for-attribute'
|
62
|
+
raise TypeError.parse(cause.getMessage)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
raise DatomicError, "Transaction failed: #{e.getMessage}"
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def retract(entity)
|
71
|
+
entity_id = entity.is_a?(Entity) ? entity.id : entity
|
72
|
+
transact([[:'db.fn/retractEntity', entity_id]])
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.convert_datoms(datoms)
|
76
|
+
case datoms
|
77
|
+
when Array
|
78
|
+
Translation.from_ruby(datoms)
|
79
|
+
when String
|
80
|
+
Utility.read_edn(datoms)
|
81
|
+
else
|
82
|
+
raise ArgumentError, 'datoms must be an Array or a String containing EDN.'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.tempid(partition=:'db.part/user', id=nil)
|
87
|
+
partition = Keyword.intern(partition.to_s.sub(/^:/, ''))
|
88
|
+
if id
|
89
|
+
Peer.tempid(partition, id)
|
90
|
+
else
|
91
|
+
Peer.tempid(partition)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.tempid?(id)
|
96
|
+
0 > case id
|
97
|
+
when Numeric
|
98
|
+
id
|
99
|
+
when Java::DatomicDb::DbId
|
100
|
+
id.get(Utility::kw('idx'))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Dalton
|
2
|
+
|
3
|
+
class Database
|
4
|
+
|
5
|
+
include Dalton::Undatomization
|
6
|
+
|
7
|
+
def initialize(datomic_database_value)
|
8
|
+
@datomic_db = datomic_database_value
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :datomic_db
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
datomic_db == other.datomic_db
|
15
|
+
end
|
16
|
+
|
17
|
+
def q(query, *args)
|
18
|
+
translated_query = Translation.from_ruby(query)
|
19
|
+
# STDERR.puts "translated_query=#{translated_query.to_edn}"
|
20
|
+
result = Peer.q(translated_query, datomic_db, *args)
|
21
|
+
Translation.from_clj(result)
|
22
|
+
rescue Java::JavaUtilConcurrent::ExecutionException => e
|
23
|
+
raise DatomicError, "Query failed: #{e.getMessage}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def entity(entity_id)
|
27
|
+
Entity.new(datomic_db.entity(Translation.from_ruby(entity_id)))
|
28
|
+
rescue Java::JavaUtilConcurrent::ExecutionException => e
|
29
|
+
raise DatomicError, "Entity retrieval failed: #{e.getMessage}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def retrieve(query, *inputs)
|
33
|
+
q(query, *inputs).lazy.map { |result| entity(result.first) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def attribute(id)
|
37
|
+
Attribute.new(datomic_db.attribute(Translation.from_ruby(id)))
|
38
|
+
end
|
39
|
+
|
40
|
+
def basis_t
|
41
|
+
datomic_db.basisT
|
42
|
+
end
|
43
|
+
|
44
|
+
def ==(other)
|
45
|
+
self.datomic_db == other.datomic_db
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Dalton
|
2
|
+
module Datomization
|
3
|
+
|
4
|
+
Utility.require_clojure('goodguide.datomizer.datomize.setup')
|
5
|
+
Utility.require_clojure('goodguide.datomizer.datomize.decode')
|
6
|
+
|
7
|
+
def set_up_datomizer
|
8
|
+
Utility.run_clojure_function('goodguide.datomizer.datomize.setup/load-datomizer-schema', datomic_connection)
|
9
|
+
Utility.run_clojure_function('goodguide.datomizer.datomize.setup/load-datomizer-functions', datomic_connection)
|
10
|
+
end
|
11
|
+
|
12
|
+
def datomize(data)
|
13
|
+
result = transact([[:'dmzr/datomize', data]])
|
14
|
+
result.resolve_tempid(data[:'db/id'])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Dalton
|
2
|
+
class Entity
|
3
|
+
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(datomic_entity)
|
7
|
+
@datomic_entity = datomic_entity
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :datomic_entity
|
11
|
+
|
12
|
+
def db
|
13
|
+
Database.new(datomic_entity.db)
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(key)
|
17
|
+
Translation.from_clj(datomic_entity.get(Translation.from_ruby(key)))
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :[], :get
|
21
|
+
|
22
|
+
def touch
|
23
|
+
@datomic_entity.touch
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def keys
|
28
|
+
datomic_entity.keySet.map{|x| x.sub(/^:/, '').to_sym}.to_a
|
29
|
+
end
|
30
|
+
|
31
|
+
def id
|
32
|
+
get(:'db/id')
|
33
|
+
end
|
34
|
+
|
35
|
+
def each
|
36
|
+
if block_given?
|
37
|
+
keys.each do |key|
|
38
|
+
yield [key, get(key)]
|
39
|
+
end
|
40
|
+
self
|
41
|
+
else
|
42
|
+
Enumerator.new(self)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
alias_method :each_pair, :each
|
46
|
+
|
47
|
+
def to_h
|
48
|
+
Hash[map {|key, value|
|
49
|
+
[key, decode(value)]
|
50
|
+
}]
|
51
|
+
end
|
52
|
+
|
53
|
+
def ==(other)
|
54
|
+
other.instance_of?(self.class) && Utility.clojure_equal?(datomic_entity, other.datomic_entity)
|
55
|
+
end
|
56
|
+
|
57
|
+
def decode(value)
|
58
|
+
case value
|
59
|
+
when Dalton::Entity
|
60
|
+
value.to_h
|
61
|
+
when Set
|
62
|
+
Set.new(value.map{|x| decode(x)})
|
63
|
+
else
|
64
|
+
Translation.from_clj(value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
DB_FUNCTION_KEY = ::Keyword.intern('db/fn')
|
69
|
+
|
70
|
+
def fn
|
71
|
+
@datomic_entity.get(DB_FUNCTION_KEY)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Dalton
|
2
|
+
class UniqueConflict < DatomicError
|
3
|
+
# TODO: [jneen] this is terrible, but error handling is not implemented at the moment.
|
4
|
+
# eventually all this data should be accessible via (ex-data e).
|
5
|
+
MESSAGE_RE =
|
6
|
+
%r(^:db[.]error/unique-conflict Unique conflict: :([a-z./-]+), value: (.*?) already held by: (\d+) asserted for: (\d+)$)o
|
7
|
+
|
8
|
+
def self.parse(message)
|
9
|
+
message =~ MESSAGE_RE
|
10
|
+
raise ArgumentError, "invalid format: #{message.inspect}" unless $~
|
11
|
+
new(
|
12
|
+
attribute: $1.to_sym,
|
13
|
+
value: $2,
|
14
|
+
existing_id: Integer($3),
|
15
|
+
new_id: Integer($4),
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :message, :attribute, :value, :existing_id, :new_id
|
20
|
+
|
21
|
+
def initialize(opts={})
|
22
|
+
@attribute = opts.fetch(:attribute)
|
23
|
+
@value = opts.fetch(:value)
|
24
|
+
@existing_id = opts.fetch(:existing_id)
|
25
|
+
@new_id = opts.fetch(:new_id)
|
26
|
+
@message = "Unique conflict: tried to assign duplicate #@attribute to #@new_id, already held by #@existing_id. value: #@value"
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
"#{self.class.name}: #@message"
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
"#<#{self.class.name}: @attribute=#@attribute @value=#@value @existing_id=#@existing_id @new_id=#@new_id>"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class TypeError < DatomicError
|
39
|
+
MESSAGE_RE = %r(^:db[.]error/wrong-type-for-attribute Value (.*?) is not a valid :(\w+) for attribute :([a-z./-]+)$)
|
40
|
+
|
41
|
+
def self.parse(message)
|
42
|
+
message =~ MESSAGE_RE
|
43
|
+
raise ArgumentError, "invalid format: #{message.inspect}" unless $~
|
44
|
+
new(
|
45
|
+
value: $1,
|
46
|
+
type: $2.to_sym,
|
47
|
+
attribute: $3.to_sym
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :message, :value, :type, :attribute
|
52
|
+
|
53
|
+
def initialize(opts={})
|
54
|
+
@value = opts.fetch(:value)
|
55
|
+
@type = opts.fetch(:type)
|
56
|
+
@attribute = opts.fetch(:attribute)
|
57
|
+
@message = "Type error: tried to set #@attribute as #@value, expected type #@type"
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
"#{self.class.name}: #@message"
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
"#<#{self.class.name}: @attribute=#@attribute @value=#@value @type=#@type>"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
java_import "datomic.Peer"
|
2
|
+
|
3
|
+
module Dalton
|
4
|
+
class TransactionResult
|
5
|
+
def initialize(result_map)
|
6
|
+
@result_map = result_map
|
7
|
+
end
|
8
|
+
|
9
|
+
def db_before
|
10
|
+
Dalton::Database.new(@result_map.get(Java::Datomic::Connection.DB_BEFORE))
|
11
|
+
end
|
12
|
+
|
13
|
+
def db_after
|
14
|
+
Dalton::Database.new(@result_map.get(Java::Datomic::Connection.DB_AFTER))
|
15
|
+
end
|
16
|
+
|
17
|
+
def tx_data
|
18
|
+
Translation.from_clj(@result_map.get(Java::Datomic::Connection.TX_DATA))
|
19
|
+
end
|
20
|
+
|
21
|
+
def raw_tempids
|
22
|
+
@result_map.get(Java::Datomic::Connection.TEMPIDS)
|
23
|
+
end
|
24
|
+
|
25
|
+
def tempids
|
26
|
+
Translation.from_clj(raw_tempids)
|
27
|
+
end
|
28
|
+
|
29
|
+
def resolve_tempid(tempid)
|
30
|
+
Peer.resolve_tempid(db_after.datomic_db, raw_tempids, tempid)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
java_import "clojure.lang.PersistentHashSet"
|
2
|
+
java_import "clojure.lang.Keyword"
|
3
|
+
|
4
|
+
module Zweikopf
|
5
|
+
module Primitive
|
6
|
+
def self.is_primitive_type?(obj) # monkey patch to remove DateTime from list of primitives and allow them to be converted. :-/
|
7
|
+
[String, Fixnum, Integer, Float, TrueClass, FalseClass].include?(obj.class)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Keyword
|
12
|
+
# Monkey patch special handling for datalog variables
|
13
|
+
def self.from_ruby(keyword)
|
14
|
+
if keyword.to_s =~ /^[?$]/
|
15
|
+
Java::ClojureLang::Symbol.intern(keyword.to_s)
|
16
|
+
else
|
17
|
+
::Keyword.intern(keyword.to_s)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Dalton
|
24
|
+
module Translation
|
25
|
+
|
26
|
+
module_function
|
27
|
+
|
28
|
+
#TODO: Fork Zweikopf, add Set handling, submit pull request
|
29
|
+
|
30
|
+
def from_clj(object)
|
31
|
+
Zweikopf::Transformer.from_clj(object) do |value|
|
32
|
+
case value
|
33
|
+
when Java::ClojureLang::Symbol
|
34
|
+
value.to_s.to_sym
|
35
|
+
when Java::JavaUtil::Set
|
36
|
+
Set.new(value.map{|x| from_clj(x)})
|
37
|
+
when Java::JavaUtil::ArrayList
|
38
|
+
value.map { |x| from_clj(x) }
|
39
|
+
when Java::Datomic::Entity, Java::DatomicQuery::EntityMap
|
40
|
+
Dalton::Entity.new(value)
|
41
|
+
when Java::JavaUtil::Date
|
42
|
+
Time.at(value.getTime / 1000).to_datetime
|
43
|
+
else
|
44
|
+
value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def from_ruby(object)
|
50
|
+
Zweikopf::Transformer.from_ruby(object) do |value|
|
51
|
+
case value
|
52
|
+
when ::Set
|
53
|
+
PersistentHashSet.create(value.map{|x| from_ruby(x)})
|
54
|
+
when Dalton::Entity
|
55
|
+
value.datomic_entity
|
56
|
+
when DateTime
|
57
|
+
Java::JavaUtil::Date.new(value.to_time.to_i * 1000)
|
58
|
+
else
|
59
|
+
value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|