sunstone 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|