sunstone 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/active_record/connection_adapters/sunstone/column.rb +19 -0
- data/lib/active_record/connection_adapters/sunstone/database_statements.rb +40 -0
- data/lib/active_record/connection_adapters/sunstone/schema_statements.rb +95 -0
- data/lib/active_record/connection_adapters/sunstone/type/date_time.rb +22 -0
- data/lib/active_record/connection_adapters/sunstone_adapter.rb +177 -0
- data/lib/arel/collectors/sunstone.rb +75 -0
- data/lib/arel/visitors/sunstone.rb +769 -0
- data/lib/ext/active_record/associations/builder/has_and_belongs_to_many.rb +48 -0
- data/lib/ext/active_record/relation.rb +26 -0
- data/lib/ext/active_record/statement_cache.rb +24 -0
- data/lib/sunstone.rb +37 -347
- data/lib/sunstone/connection.rb +337 -0
- data/sunstone.gemspec +3 -2
- data/test/sunstone/connection_test.rb +319 -0
- data/test/sunstone/parser_test.rb +21 -21
- metadata +30 -36
- data/lib/sunstone/model.rb +0 -23
- data/lib/sunstone/model/attributes.rb +0 -99
- data/lib/sunstone/model/persistence.rb +0 -168
- data/lib/sunstone/schema.rb +0 -38
- data/lib/sunstone/type/boolean.rb +0 -19
- data/lib/sunstone/type/date_time.rb +0 -20
- data/lib/sunstone/type/decimal.rb +0 -19
- data/lib/sunstone/type/integer.rb +0 -17
- data/lib/sunstone/type/mutable.rb +0 -16
- data/lib/sunstone/type/string.rb +0 -18
- data/lib/sunstone/type/value.rb +0 -97
- data/test/sunstone/model/associations_test.rb +0 -55
- data/test/sunstone/model/attributes_test.rb +0 -60
- data/test/sunstone/model/persistence_test.rb +0 -173
- data/test/sunstone/model_test.rb +0 -11
- data/test/sunstone/schema_test.rb +0 -25
- data/test/sunstone/type/boolean_test.rb +0 -24
- data/test/sunstone/type/date_time_test.rb +0 -31
- data/test/sunstone/type/decimal_test.rb +0 -27
- data/test/sunstone/type/integer_test.rb +0 -29
- data/test/sunstone/type/string_test.rb +0 -54
- data/test/sunstone/type/value_test.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c22d4667778ecfe53034454215950b7c93c82dac
|
4
|
+
data.tar.gz: fab3525c04c36627f5df0788ab9357fe5b020135
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0758fa4348af24daae4be0a7217385e7e648dd8ff8790521a12411559b3b0f15892028eb6c2398b85e67546b40fc1c9c2edfa3a93d6bb2b07f920411e11cb17f
|
7
|
+
data.tar.gz: ef3c3385aa3502ce96e787e53b653cb8b9901fc9dcd11b756c9098e24ad3e3d2ea30abb8017913df15d6bcffc4b527d96f8185d4eb0d63415539193bbc510221
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
# Sunstone-specific extensions to column definitions in a table.
|
4
|
+
class SunstoneColumn < Column #:nodoc:
|
5
|
+
attr_accessor :array
|
6
|
+
|
7
|
+
def initialize(name, cast_type, options={})
|
8
|
+
@primary_key = (options['primary_key'] == true)
|
9
|
+
@array = !!options['array']
|
10
|
+
super(name, options['default'], cast_type, nil, options['null'])
|
11
|
+
end
|
12
|
+
|
13
|
+
def primary_key?
|
14
|
+
@primary_key
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Sunstone
|
4
|
+
module DatabaseStatements
|
5
|
+
|
6
|
+
# Converts an arel AST to a Sunstone API Request
|
7
|
+
def to_sar(arel, bvs)
|
8
|
+
if arel.respond_to?(:ast)
|
9
|
+
collected = visitor.accept(arel.ast, collector)
|
10
|
+
collected.compile(bvs)
|
11
|
+
else
|
12
|
+
arel
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns an ActiveRecord::Result instance.
|
17
|
+
def select_all(arel, name = nil, binds = [], &block)
|
18
|
+
exec_query(arel, name, binds)
|
19
|
+
end
|
20
|
+
|
21
|
+
def exec_query(arel, name = 'SAR', binds = [])
|
22
|
+
result = exec(to_sar(arel, binds), name)
|
23
|
+
|
24
|
+
if result.is_a?(Array)
|
25
|
+
ActiveRecord::Result.new(result[0] ? result[0].keys : [], result.map{|r| r.values})
|
26
|
+
else
|
27
|
+
# this is a count.. yea i know..
|
28
|
+
ActiveRecord::Result.new(['all'], [[result]], {:all => type_map.lookup('integer')})
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Sunstone
|
4
|
+
|
5
|
+
module SchemaStatements
|
6
|
+
|
7
|
+
# Returns true if table exists.
|
8
|
+
# If the schema is not specified as part of +name+ then it will only find tables within
|
9
|
+
# the current schema search path (regardless of permissions to access tables in other schemas)
|
10
|
+
def table_exists?(name)
|
11
|
+
schema_definition[name] != nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns the list of all column definitions for a table.
|
15
|
+
def columns(table_name)
|
16
|
+
# Limit, precision, and scale are all handled by the superclass.
|
17
|
+
column_definitions(table_name).map do |column_name, options|
|
18
|
+
new_column(column_name, lookup_cast_type(options['type']), options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the list of a table's column names, data types, and default values.
|
23
|
+
#
|
24
|
+
# Query implementation notes:
|
25
|
+
# - format_type includes the column size constraint, e.g. varchar(50)
|
26
|
+
# - ::regclass is a function that gives the id for a table name
|
27
|
+
def column_definitions(table_name) # :nodoc:
|
28
|
+
definition = schema_definition[table_name]
|
29
|
+
raise ActiveRecord::StatementInvalid, "Table \"#{table_name}\" does not exist" if definition.nil?
|
30
|
+
|
31
|
+
definition
|
32
|
+
end
|
33
|
+
|
34
|
+
def schema_definition
|
35
|
+
exec( Arel::Table.new(:schema).project )
|
36
|
+
end
|
37
|
+
|
38
|
+
def tables
|
39
|
+
Wankel.parse(@connection.get('/schema').body, :symbolize_keys => true).keys
|
40
|
+
end
|
41
|
+
|
42
|
+
def new_column(name, cast_type, options={}) # :nodoc:
|
43
|
+
SunstoneColumn.new(name, cast_type, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
# TODO: def encoding
|
47
|
+
|
48
|
+
# Returns just a table's primary key
|
49
|
+
def primary_key(table)
|
50
|
+
columns(table).find{ |c| c.primary_key? }.name
|
51
|
+
end
|
52
|
+
|
53
|
+
# TODO: do we need this?
|
54
|
+
# Maps logical Rails types to PostgreSQL-specific data types.
|
55
|
+
# def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
56
|
+
# case type.to_s
|
57
|
+
# when 'binary'
|
58
|
+
# # PostgreSQL doesn't support limits on binary (bytea) columns.
|
59
|
+
# # The hard limit is 1Gb, because of a 32-bit size field, and TOAST.
|
60
|
+
# case limit
|
61
|
+
# when nil, 0..0x3fffffff; super(type)
|
62
|
+
# else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
|
63
|
+
# end
|
64
|
+
# when 'text'
|
65
|
+
# # PostgreSQL doesn't support limits on text columns.
|
66
|
+
# # The hard limit is 1Gb, according to section 8.3 in the manual.
|
67
|
+
# case limit
|
68
|
+
# when nil, 0..0x3fffffff; super(type)
|
69
|
+
# else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.")
|
70
|
+
# end
|
71
|
+
# when 'integer'
|
72
|
+
# return 'integer' unless limit
|
73
|
+
#
|
74
|
+
# case limit
|
75
|
+
# when 1, 2; 'smallint'
|
76
|
+
# when 3, 4; 'integer'
|
77
|
+
# when 5..8; 'bigint'
|
78
|
+
# else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
|
79
|
+
# end
|
80
|
+
# when 'datetime'
|
81
|
+
# return super unless precision
|
82
|
+
#
|
83
|
+
# case precision
|
84
|
+
# when 0..6; "timestamp(#{precision})"
|
85
|
+
# else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6")
|
86
|
+
# end
|
87
|
+
# else
|
88
|
+
# super
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Sunstone
|
4
|
+
module Type
|
5
|
+
class DateTime < ActiveRecord::Type::DateTime
|
6
|
+
|
7
|
+
def type_cast_for_database(value)
|
8
|
+
super(value).iso8601(3) if value
|
9
|
+
end
|
10
|
+
|
11
|
+
def cast_value(string)
|
12
|
+
return string unless string.is_a?(::String)
|
13
|
+
return if string.empty?
|
14
|
+
|
15
|
+
::DateTime.iso8601(string) || fast_string_to_time(string) || fallback_string_to_time(string)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
2
|
+
require 'active_record/connection_adapters/statement_pool'
|
3
|
+
|
4
|
+
require 'arel/visitors/sunstone'
|
5
|
+
require 'arel/collectors/sunstone'
|
6
|
+
|
7
|
+
require 'active_record/connection_adapters/sunstone/database_statements'
|
8
|
+
require 'active_record/connection_adapters/sunstone/schema_statements'
|
9
|
+
require 'active_record/connection_adapters/sunstone/column'
|
10
|
+
|
11
|
+
require 'active_record/connection_adapters/sunstone/type/date_time'
|
12
|
+
|
13
|
+
module ActiveRecord
|
14
|
+
module ConnectionHandling # :nodoc:
|
15
|
+
|
16
|
+
VALID_SUNSTONE_CONN_PARAMS = [:site, :host, :port, :api_key, :use_ssl]
|
17
|
+
|
18
|
+
# Establishes a connection to the database that's used by all Active Record
|
19
|
+
# objects
|
20
|
+
def sunstone_connection(config)
|
21
|
+
conn_params = config.symbolize_keys
|
22
|
+
|
23
|
+
conn_params.delete_if { |_, v| v.nil? }
|
24
|
+
|
25
|
+
# Map ActiveRecords param names to PGs.
|
26
|
+
conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
|
27
|
+
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
|
28
|
+
if conn_params[:site]
|
29
|
+
uri = URI.parse(conn_params.delete(:site))
|
30
|
+
conn_params[:api_key] ||= (uri.user ? CGI.unescape(uri.user) : nil)
|
31
|
+
conn_params[:host] ||= uri.host
|
32
|
+
conn_params[:port] ||= uri.port
|
33
|
+
conn_params[:use_ssl] ||= (uri.scheme == 'https')
|
34
|
+
end
|
35
|
+
|
36
|
+
# Forward only valid config params to PGconn.connect.
|
37
|
+
conn_params.keep_if { |k, _| VALID_SUNSTONE_CONN_PARAMS.include?(k) }
|
38
|
+
|
39
|
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
|
40
|
+
# so just pass a nil connection object for the time being.
|
41
|
+
ConnectionAdapters::SunstoneAPIAdapter.new(nil, logger, conn_params, config)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module ConnectionAdapters
|
46
|
+
# The SunstoneAPI adapter.
|
47
|
+
#
|
48
|
+
# Options:
|
49
|
+
#
|
50
|
+
# * <tt>:host</tt> - Defaults to a Unix-domain socket in /tmp. On machines
|
51
|
+
# without Unix-domain sockets, the default is to connect to localhost.
|
52
|
+
# * <tt>:port</tt> - Defaults to 5432.
|
53
|
+
# * <tt>:username</tt> - The API key to connect with
|
54
|
+
# * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO
|
55
|
+
# <encoding></tt> call on the connection.
|
56
|
+
class SunstoneAPIAdapter < AbstractAdapter
|
57
|
+
ADAPTER_NAME = 'Sunstone'
|
58
|
+
|
59
|
+
NATIVE_DATABASE_TYPES = {
|
60
|
+
string: { name: "string" },
|
61
|
+
number: { name: "number" },
|
62
|
+
json: { name: "json" },
|
63
|
+
boolean: { name: "boolean" }
|
64
|
+
}
|
65
|
+
|
66
|
+
include Sunstone::DatabaseStatements
|
67
|
+
include Sunstone::SchemaStatements
|
68
|
+
|
69
|
+
# Returns 'SunstoneAPI' as adapter name for identification purposes.
|
70
|
+
def adapter_name
|
71
|
+
ADAPTER_NAME
|
72
|
+
end
|
73
|
+
|
74
|
+
# Adds `:array` option to the default set provided by the AbstractAdapter
|
75
|
+
def prepare_column_options(column, types) # :nodoc:
|
76
|
+
spec = super
|
77
|
+
spec[:array] = 'true' if column.respond_to?(:array) && column.array
|
78
|
+
spec
|
79
|
+
end
|
80
|
+
|
81
|
+
# Initializes and connects a SunstoneAPI adapter.
|
82
|
+
def initialize(connection, logger, connection_parameters, config)
|
83
|
+
super(connection, logger)
|
84
|
+
|
85
|
+
@visitor = Arel::Visitors::Sunstone.new self
|
86
|
+
@connection_parameters, @config = connection_parameters, config
|
87
|
+
|
88
|
+
connect
|
89
|
+
|
90
|
+
@type_map = Type::HashLookupTypeMap.new
|
91
|
+
initialize_type_map(type_map)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Is this connection alive and ready for queries?
|
95
|
+
def active?
|
96
|
+
@connection.ping
|
97
|
+
true
|
98
|
+
rescue Net::HTTPExceptions
|
99
|
+
false
|
100
|
+
end
|
101
|
+
|
102
|
+
# TODO: this doesn't work yet
|
103
|
+
# Close then reopen the connection.
|
104
|
+
def reconnect!
|
105
|
+
super
|
106
|
+
@connection.reset
|
107
|
+
configure_connection
|
108
|
+
end
|
109
|
+
|
110
|
+
# TODO don't know about this yet
|
111
|
+
def reset!
|
112
|
+
configure_connection
|
113
|
+
end
|
114
|
+
|
115
|
+
# TODO: deal with connection.close
|
116
|
+
# Disconnects from the database if already connected. Otherwise, this
|
117
|
+
# method does nothing.
|
118
|
+
def disconnect!
|
119
|
+
super
|
120
|
+
@connection.close rescue nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def native_database_types #:nodoc:
|
124
|
+
NATIVE_DATABASE_TYPES
|
125
|
+
end
|
126
|
+
|
127
|
+
def use_insert_returning?
|
128
|
+
true
|
129
|
+
end
|
130
|
+
|
131
|
+
def valid_type?(type)
|
132
|
+
!native_database_types[type].nil?
|
133
|
+
end
|
134
|
+
|
135
|
+
def update_table_definition(table_name, base) #:nodoc:
|
136
|
+
SunstoneAPI::Table.new(table_name, base)
|
137
|
+
end
|
138
|
+
|
139
|
+
def collector
|
140
|
+
Arel::Collectors::Sunstone.new
|
141
|
+
end
|
142
|
+
|
143
|
+
def server_config
|
144
|
+
Wankel.parse(@connection.get("/configuration").body)
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def initialize_type_map(m) # :nodoc:
|
150
|
+
m.register_type 'boolean', Type::Boolean.new
|
151
|
+
m.register_type 'string', Type::String.new
|
152
|
+
m.register_type 'integer', Type::Integer.new
|
153
|
+
m.register_type 'decimal', Type::Decimal.new
|
154
|
+
m.register_type 'datetime', Sunstone::Type::DateTime.new
|
155
|
+
m.register_type 'hash', Type::Value.new
|
156
|
+
end
|
157
|
+
|
158
|
+
def exec(arel, name='SAR', binds=[])
|
159
|
+
# result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
|
160
|
+
# exec_cache(sql, name, binds)
|
161
|
+
sar = to_sar(arel, binds)
|
162
|
+
|
163
|
+
log(sar.is_a?(String) ? sar : "#{sar.class} #{CGI.unescape(sar.path)}", name) { Wankel.parse(@connection.send_request(sar).body) }
|
164
|
+
end
|
165
|
+
|
166
|
+
# Connects to a Sunstone API server and sets up the adapter depending on
|
167
|
+
# the connected server's characteristics.
|
168
|
+
def connect
|
169
|
+
@connection = ::Sunstone::Connection.new(@connection_parameters)
|
170
|
+
end
|
171
|
+
|
172
|
+
def create_table_definition(name, temporary, options, as = nil) # :nodoc:
|
173
|
+
SunstoneAPI::TableDefinition.new native_database_types, name, temporary, options, as
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Arel
|
2
|
+
module Collectors
|
3
|
+
class Sunstone < Arel::Collectors::Bind
|
4
|
+
|
5
|
+
attr_accessor :request_type, :table, :where, :limit, :offset, :order, :operation, :columns
|
6
|
+
|
7
|
+
def substitute_binds hash, bvs
|
8
|
+
if hash.is_a?(Array)
|
9
|
+
hash.map { |w| substitute_binds(w, bvs) }
|
10
|
+
else
|
11
|
+
newhash = {}
|
12
|
+
hash.each do |k, v|
|
13
|
+
if Arel::Nodes::BindParam === v
|
14
|
+
newhash[k] = bvs.shift.last
|
15
|
+
elsif v.is_a?(Hash)
|
16
|
+
newhash[k] = substitute_binds(v, bvs)
|
17
|
+
else
|
18
|
+
newhash[k] = v
|
19
|
+
end
|
20
|
+
end
|
21
|
+
newhash
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def value
|
26
|
+
flatten_nested(where).flatten
|
27
|
+
end
|
28
|
+
|
29
|
+
def flatten_nested(obj)
|
30
|
+
if obj.is_a?(Array)
|
31
|
+
obj.map { |w| flatten_nested(w) }
|
32
|
+
elsif obj.is_a?(Hash)
|
33
|
+
obj.map{ |k,v| [k, flatten_nested(v)] }.flatten
|
34
|
+
else
|
35
|
+
obj
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def compile bvs
|
40
|
+
path = "/#{table}"
|
41
|
+
|
42
|
+
case operation
|
43
|
+
when :count, :average, :min, :max
|
44
|
+
path += "/#{operation}"
|
45
|
+
end
|
46
|
+
|
47
|
+
get_params = {}
|
48
|
+
|
49
|
+
if where
|
50
|
+
get_params[:where] = substitute_binds(where.clone, bvs)
|
51
|
+
if get_params[:where].size == 1
|
52
|
+
get_params[:where] = get_params[:where].pop
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
get_params[:limit] = limit if limit
|
57
|
+
get_params[:offset] = offset if offset
|
58
|
+
get_params[:order] = order if order
|
59
|
+
get_params[:columns] = columns if columns
|
60
|
+
|
61
|
+
if get_params.size > 0
|
62
|
+
path += '?' + get_params.to_param
|
63
|
+
end
|
64
|
+
|
65
|
+
request = request_type.new(path)
|
66
|
+
|
67
|
+
request
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
|