activerecord-jdbccassandra-adapter 0.0.1
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.
- data/LICENSE.txt +22 -0
- data/README.md +7 -0
- data/Rakefile +15 -0
- data/lib/active_record/connection_adapters/jdbccassandra_adapter.rb +6 -0
- data/lib/activerecord-jdbccassandra-adapter.rb +3 -0
- data/lib/arjdbc/cassandra.rb +275 -0
- data/lib/arjdbc/cassandra/adapter.rb +42 -0
- data/lib/arjdbc/cassandra/connection_methods.rb +27 -0
- data/lib/arjdbc/cassandra/version.rb +5 -0
- metadata +86 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 James Thompson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler/gem_helper'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
module Bundler
|
5
|
+
class GemHelper
|
6
|
+
def guard_already_tagged
|
7
|
+
# parent project performs the tag
|
8
|
+
end
|
9
|
+
def tag_version
|
10
|
+
Bundler.ui.confirm "Parent project tagged #{version_tag}"
|
11
|
+
yield if block_given?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
@@ -0,0 +1,6 @@
|
|
1
|
+
# NOTE: required by AR resolver with 'jdbccassandra' adapter configuration :
|
2
|
+
# require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
|
3
|
+
# we should make sure a jdbccassandr_connection is setup on ActiveRecord::Base
|
4
|
+
require 'arjdbc/cassandra'
|
5
|
+
# all setup should be performed in arjdbc/cassandra to avoid circular requires
|
6
|
+
# this should not be required from any loads performed by arjdbc/cassandra code
|
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'arjdbc/jdbc'
|
2
|
+
require 'arjdbc/cassandra/version'
|
3
|
+
require 'arjdbc/cassandra/jdbc_connection'
|
4
|
+
require 'arjdbc/cassandra/column'
|
5
|
+
require 'arjdbc/cassandra/error'
|
6
|
+
require 'arjdbc/cassandra/adapter'
|
7
|
+
require 'arjdbc/cassandra/connection_methods'
|
8
|
+
|
9
|
+
module ArJdbc
|
10
|
+
module Cassandra
|
11
|
+
def self.extended(adapter)
|
12
|
+
adapter.configure_connection
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.column_selector
|
16
|
+
[ /cassandra/i, lambda { |_,column| column.extend(::ArJdbc::Cassandra::Column) } ]
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.jdbc_connection_class
|
20
|
+
::ActiveRecord::ConnectionAdapters::CassandraJdbcConnection
|
21
|
+
end
|
22
|
+
|
23
|
+
NATIVE_DATABASE_TYPES = {
|
24
|
+
:primary_key => 'uuid PRIMARY KEY',
|
25
|
+
:string => { :name => 'varchar' },
|
26
|
+
:text => { :name => 'text' },
|
27
|
+
:integer => { :name => 'int' },
|
28
|
+
:float => { :name => 'float' },
|
29
|
+
:decimal => { :name => 'decimal' },
|
30
|
+
:datetime => { :name => 'timestamp' },
|
31
|
+
:timestamp => { :name => 'timestamp' },
|
32
|
+
:time => { :name => 'timestamp' },
|
33
|
+
:date => { :name => 'timestamp' },
|
34
|
+
:binary => { :name => 'blob' },
|
35
|
+
:boolean => { :name => 'boolean' },
|
36
|
+
# Extended support for Cassandra types
|
37
|
+
:ascii => { :name => 'ascii' },
|
38
|
+
:bigint => { :name => 'bigint' },
|
39
|
+
:blob => { :name => 'blob' },
|
40
|
+
:counter => { :name => 'counter' },
|
41
|
+
:double => { :name => 'double' },
|
42
|
+
:inet => { :name => 'inet' },
|
43
|
+
:timeuuid => { :name => 'timeuuid' },
|
44
|
+
:uuid => { :name => 'uuid' },
|
45
|
+
:varchar => { :name => 'varchar' },
|
46
|
+
:varint => { :name => 'varint' }
|
47
|
+
}
|
48
|
+
|
49
|
+
def native_database_types
|
50
|
+
NATIVE_DATABASE_TYPES
|
51
|
+
end
|
52
|
+
|
53
|
+
ADAPTER_NAME = 'Cassandra'.freeze
|
54
|
+
|
55
|
+
def adapter_name #:nodoc:
|
56
|
+
ADAPTER_NAME
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.arel2_visitors(config)
|
60
|
+
{
|
61
|
+
'cassandra' => ::Arel::Visitors::ToSql,
|
62
|
+
'jdbccassandra' => ::Arel::Visitors::ToSql
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def case_sensitive_modifier(node)
|
67
|
+
Arel::Nodes::Bin.new(node)
|
68
|
+
end
|
69
|
+
|
70
|
+
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
71
|
+
where_sql
|
72
|
+
end
|
73
|
+
|
74
|
+
def supports_migrations?
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
def supports_primary_key? # :nodoc:
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
def supports_bulk_alter? # :nodoc:
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
def supports_index_sort_order? # :nodoc:
|
87
|
+
false
|
88
|
+
end
|
89
|
+
|
90
|
+
def supports_transaction_isolation? # :nodoc:
|
91
|
+
false
|
92
|
+
end
|
93
|
+
|
94
|
+
def supports_views? # :nodoc:
|
95
|
+
false
|
96
|
+
end
|
97
|
+
|
98
|
+
def supports_savepoints? # :nodoc:
|
99
|
+
false
|
100
|
+
end
|
101
|
+
|
102
|
+
# DATABASE STATEMENTS ======================================
|
103
|
+
|
104
|
+
def exec_insert(sql, name, binds)
|
105
|
+
execute sql, name, binds
|
106
|
+
end
|
107
|
+
alias :exec_update :exec_insert
|
108
|
+
alias :exec_delete :exec_insert
|
109
|
+
|
110
|
+
def select(sql, name = nil, binds = [])
|
111
|
+
query = sql.gsub(/[0-9a-z_-]+\.\*/i, '*')
|
112
|
+
execute query
|
113
|
+
end
|
114
|
+
|
115
|
+
# SCHEMA STATEMENTS ========================================
|
116
|
+
|
117
|
+
def structure_dump #:nodoc:
|
118
|
+
execute('DESCRIBE SCHEMA')
|
119
|
+
end
|
120
|
+
|
121
|
+
def recreate_database(name, options = {}) #:nodoc:
|
122
|
+
drop_database(name)
|
123
|
+
create_database(name, options)
|
124
|
+
end
|
125
|
+
|
126
|
+
def create_database(name, options = {}) #:nodoc:
|
127
|
+
query = "CREATE KEYSPACE #{name} WITH strategy_class = #{options[:strategy_class] || 'SimpleStrategy'}"
|
128
|
+
|
129
|
+
if options[:strategy_options]
|
130
|
+
if options[:strategy_options].is_a?(Array)
|
131
|
+
options[:strategy_options].each do |strategy_option|
|
132
|
+
query += " AND strategy_options:#{strategy_option}"
|
133
|
+
end
|
134
|
+
else
|
135
|
+
query += " AND strategy_options:#{options[:strategy_options]}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
execute query
|
140
|
+
end
|
141
|
+
|
142
|
+
def drop_database(name) #:nodoc:
|
143
|
+
execute "DROP KEYSPACE #{name}"
|
144
|
+
end
|
145
|
+
|
146
|
+
def create_table(name, options = {}) #:nodoc:
|
147
|
+
td = table_definition
|
148
|
+
td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
|
149
|
+
|
150
|
+
yield td if block_given?
|
151
|
+
|
152
|
+
create_sql = "CREATE TABLE #{name} ("
|
153
|
+
create_sql << td.to_sql
|
154
|
+
create_sql << ") #{options[:options]}"
|
155
|
+
execute create_sql
|
156
|
+
end
|
157
|
+
|
158
|
+
def drop_table(name) #:nodoc:
|
159
|
+
execute "DROP TABLE #{name}"
|
160
|
+
end
|
161
|
+
|
162
|
+
#def rename_table(name, new_name)
|
163
|
+
# execute "RENAME TABLE #{quote_table_name(name)} TO #{quote_table_name(new_name)}"
|
164
|
+
#end
|
165
|
+
|
166
|
+
#def remove_index!(table_name, index_name) #:nodoc:
|
167
|
+
# # missing table_name quoting in AR-2.3
|
168
|
+
# execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
|
169
|
+
#end
|
170
|
+
|
171
|
+
#def add_column(table_name, column_name, type, options = {})
|
172
|
+
# add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
173
|
+
# add_column_options!(add_column_sql, options)
|
174
|
+
# add_column_position!(add_column_sql, options)
|
175
|
+
# execute(add_column_sql)
|
176
|
+
#end
|
177
|
+
|
178
|
+
#def change_column_default(table_name, column_name, default) #:nodoc:
|
179
|
+
# column = column_for(table_name, column_name)
|
180
|
+
# change_column table_name, column_name, column.sql_type, :default => default
|
181
|
+
#end
|
182
|
+
|
183
|
+
#def change_column_null(table_name, column_name, null, default = nil)
|
184
|
+
# column = column_for(table_name, column_name)
|
185
|
+
#
|
186
|
+
# unless null || default.nil?
|
187
|
+
# execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
188
|
+
# end
|
189
|
+
#
|
190
|
+
# change_column table_name, column_name, column.sql_type, :null => null
|
191
|
+
#end
|
192
|
+
|
193
|
+
#def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
194
|
+
# column = column_for(table_name, column_name)
|
195
|
+
#
|
196
|
+
# unless options_include_default?(options)
|
197
|
+
# options[:default] = column.default
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# unless options.has_key?(:null)
|
201
|
+
# options[:null] = column.null
|
202
|
+
# end
|
203
|
+
#
|
204
|
+
# change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
205
|
+
# add_column_options!(change_column_sql, options)
|
206
|
+
# add_column_position!(change_column_sql, options)
|
207
|
+
# execute(change_column_sql)
|
208
|
+
#end
|
209
|
+
|
210
|
+
#def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
211
|
+
# options = {}
|
212
|
+
# if column = columns(table_name).find { |c| c.name == column_name.to_s }
|
213
|
+
# options[:default] = column.default
|
214
|
+
# options[:null] = column.null
|
215
|
+
# else
|
216
|
+
# raise ActiveRecord::ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
217
|
+
# end
|
218
|
+
# current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
|
219
|
+
# rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
|
220
|
+
# add_column_options!(rename_column_sql, options)
|
221
|
+
# execute(rename_column_sql)
|
222
|
+
#end
|
223
|
+
|
224
|
+
#def add_limit_offset!(sql, options) #:nodoc:
|
225
|
+
# limit, offset = options[:limit], options[:offset]
|
226
|
+
# if limit && offset
|
227
|
+
# sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}"
|
228
|
+
# elsif limit
|
229
|
+
# sql << " LIMIT #{sanitize_limit(limit)}"
|
230
|
+
# elsif offset
|
231
|
+
# sql << " OFFSET #{offset.to_i}"
|
232
|
+
# end
|
233
|
+
# sql
|
234
|
+
#end
|
235
|
+
|
236
|
+
#def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
237
|
+
# case type.to_s
|
238
|
+
# when 'binary'
|
239
|
+
# case limit
|
240
|
+
# when 0..0xfff; "varbinary(#{limit})"
|
241
|
+
# when nil; "blob"
|
242
|
+
# when 0x1000..0xffffffff; "blob(#{limit})"
|
243
|
+
# else raise(ActiveRecordError, "No binary type has character length #{limit}")
|
244
|
+
# end
|
245
|
+
# when 'integer'
|
246
|
+
# case limit
|
247
|
+
# when 1; 'tinyint'
|
248
|
+
# when 2; 'smallint'
|
249
|
+
# when 3; 'mediumint'
|
250
|
+
# when nil, 4, 11; 'int(11)' # compatibility with MySQL default
|
251
|
+
# when 5..8; 'bigint'
|
252
|
+
# else raise(ActiveRecordError, "No integer type has byte size #{limit}")
|
253
|
+
# end
|
254
|
+
# when 'text'
|
255
|
+
# case limit
|
256
|
+
# when 0..0xff; 'tinytext'
|
257
|
+
# when nil, 0x100..0xffff; 'text'
|
258
|
+
# when 0x10000..0xffffff; 'mediumtext'
|
259
|
+
# when 0x1000000..0xffffffff; 'longtext'
|
260
|
+
# else raise(ActiveRecordError, "No text type has character length #{limit}")
|
261
|
+
# end
|
262
|
+
# else
|
263
|
+
# super
|
264
|
+
# end
|
265
|
+
#end
|
266
|
+
|
267
|
+
#def add_column_position!(sql, options)
|
268
|
+
# if options[:first]
|
269
|
+
# sql << " FIRST"
|
270
|
+
# elsif options[:after]
|
271
|
+
# sql << " AFTER #{quote_column_name(options[:after])}"
|
272
|
+
# end
|
273
|
+
#end
|
274
|
+
end
|
275
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
# Make sure we don't interfere with a pure Ruby version
|
4
|
+
remove_const(:CassandraAdapter) if const_defined?(:CassandraAdapter)
|
5
|
+
|
6
|
+
class CassandraAdapter < JdbcAdapter
|
7
|
+
include ::ArJdbc::Cassandra
|
8
|
+
|
9
|
+
def jdbc_connection_class(spec)
|
10
|
+
::ArJdbc::Cassandra.jdbc_connection_class
|
11
|
+
end
|
12
|
+
|
13
|
+
def jdbc_column_class
|
14
|
+
CassandraColumn
|
15
|
+
end
|
16
|
+
#alias_chained_method :columns, :query_cache, :jdbc_columns
|
17
|
+
|
18
|
+
# some QUOTING caching :
|
19
|
+
|
20
|
+
@@quoted_table_names = {}
|
21
|
+
|
22
|
+
def quote_table_name(name)
|
23
|
+
unless quoted = @@quoted_table_names[name]
|
24
|
+
quoted = super
|
25
|
+
@@quoted_table_names[name] = quoted.freeze
|
26
|
+
end
|
27
|
+
quoted
|
28
|
+
end
|
29
|
+
|
30
|
+
@@quoted_column_names = {}
|
31
|
+
|
32
|
+
def quote_column_name(name)
|
33
|
+
unless quoted = @@quoted_column_names[name]
|
34
|
+
quoted = super
|
35
|
+
@@quoted_column_names[name] = quoted.freeze
|
36
|
+
end
|
37
|
+
quoted
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
class << self
|
3
|
+
def cassandra_connection(config)
|
4
|
+
begin
|
5
|
+
require 'jdbc/cassandra'
|
6
|
+
::Jdbc::Cassandra.load_driver(:require) if defined?(::Jdbc::Cassandra.load_driver)
|
7
|
+
rescue LoadError # assuming driver.jar is on the class-path
|
8
|
+
end
|
9
|
+
|
10
|
+
config[:host] ||= '127.0.0.1'
|
11
|
+
config[:port] ||= 9160
|
12
|
+
config[:url] ||= "jdbc:cassandra://#{config[:host]}:#{config[:port]}/#{config[:database]}"
|
13
|
+
config[:driver] ||= defined?(::Jdbc::Cassandra.driver_name) ? ::Jdbc::Cassandra.driver_name : 'org.apache.cassandra.cql.jdbc.CassandraDriver'
|
14
|
+
config[:adapter_class] = ActiveRecord::ConnectionAdapters::CassandraAdapter
|
15
|
+
config[:adapter_spec] = ::ArJdbc::Cassandra
|
16
|
+
|
17
|
+
options = (config[:options] ||= {})
|
18
|
+
#options['zeroDateTimeBehavior'] ||= 'convertToNull'
|
19
|
+
#options['jdbcCompliantTruncation'] ||= 'false'
|
20
|
+
#options['useUnicode'] ||= 'true'
|
21
|
+
#options['characterEncoding'] = config[:encoding] || 'utf8'
|
22
|
+
|
23
|
+
jdbc_connection(config)
|
24
|
+
end
|
25
|
+
alias_method :jdbccassandra_connection, :cassandra_connection
|
26
|
+
end
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-jdbccassandra-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- James Thompson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord-jdbc-adapter
|
16
|
+
version_requirements: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ~>
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 1.2.0
|
21
|
+
none: false
|
22
|
+
requirement: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.2.0
|
27
|
+
none: false
|
28
|
+
prerelease: false
|
29
|
+
type: :runtime
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: jdbc-cassandra
|
32
|
+
version_requirements: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - ~>
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 1.2.5
|
37
|
+
none: false
|
38
|
+
requirement: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ~>
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 1.2.5
|
43
|
+
none: false
|
44
|
+
prerelease: false
|
45
|
+
type: :runtime
|
46
|
+
description: Install this gem to use Cassandra with JRuby on Rails.
|
47
|
+
email:
|
48
|
+
- james@plainprograms.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- Rakefile
|
54
|
+
- README.md
|
55
|
+
- LICENSE.txt
|
56
|
+
- lib/activerecord-jdbccassandra-adapter.rb
|
57
|
+
- lib/active_record/connection_adapters/jdbccassandra_adapter.rb
|
58
|
+
- lib/arjdbc/cassandra.rb
|
59
|
+
- lib/arjdbc/cassandra/version.rb
|
60
|
+
- lib/arjdbc/cassandra/adapter.rb
|
61
|
+
- lib/arjdbc/cassandra/connection_methods.rb
|
62
|
+
homepage: https://github.com/plainprogrammer/activerecord-jdbccassandra-adapter
|
63
|
+
licenses: []
|
64
|
+
post_install_message:
|
65
|
+
rdoc_options: []
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
none: false
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
none: false
|
80
|
+
requirements: []
|
81
|
+
rubyforge_project: ''
|
82
|
+
rubygems_version: 1.8.24
|
83
|
+
signing_key:
|
84
|
+
specification_version: 3
|
85
|
+
summary: Cassandra JDBC adapter for JRuby on Rails.
|
86
|
+
test_files: []
|