Skip to content

Commit 1d94f24

Browse files
authored
Merge pull request #8 from saveriomiroddi/native_json_data_type_support
Add native JSON data type support, through a new adapter
2 parents 0cb3978 + 46bdfeb commit 1d94f24

File tree

10 files changed

+127
-29
lines changed

10 files changed

+127
-29
lines changed

.rubocop_todo.yml

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,37 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2018-02-12 18:54:36 +0100 using RuboCop version 0.52.1.
3+
# on 2018-02-12 18:57:06 +0100 using RuboCop version 0.52.1.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
77
# versions of RuboCop, may require this file to be generated again.
88

9-
# Offense count: 4
9+
# Offense count: 5
1010
Style/Documentation:
1111
Exclude:
1212
- 'spec/**/*'
1313
- 'test/**/*'
14-
- 'lib/active_record/connection_adapters/schema_definitions.rb'
14+
- 'lib/active_record/connection_adapters/abstract/json_schema_definitions.rb'
15+
- 'lib/active_record/connection_adapters/mysql2_json_adapter.rb'
1516
- 'lib/active_record/type/json.rb'
16-
- 'lib/json_on_rails.rb'
1717

18-
# Offense count: 1
18+
# Offense count: 2
1919
# Configuration parameters: MinBodyLength.
2020
Style/GuardClause:
2121
Exclude:
22+
- 'lib/active_record/connection_adapters/mysql2_json_adapter.rb'
2223
- 'lib/active_record/type/json.rb'
24+
25+
# Offense count: 1
26+
# Cop supports --auto-correct.
27+
Style/IfUnlessModifier:
28+
Exclude:
29+
- 'lib/active_record/connection_adapters/mysql2_json_adapter.rb'
30+
31+
# Offense count: 1
32+
# Cop supports --auto-correct.
33+
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
34+
# SupportedStyles: slashes, percent_r, mixed
35+
Style/RegexpLiteral:
36+
Exclude:
37+
- 'lib/active_record/connection_adapters/mysql2_json_adapter.rb'

README.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ that's all!
3131

3232
## Usage/example
3333

34+
Change the `mysql` connection adapter to `mysql2_json`, in `config/database.yml`:
35+
36+
```yaml
37+
default: &default
38+
adapter: mysql2_json
39+
```
40+
3441
Create a table:
3542
3643
```
@@ -54,13 +61,10 @@ class CreateUsers < ActiveRecord::Migration
5461
end
5562
```
5663

57-
define the model:
64+
define the model (rails will automatically pick up the data type):
5865

5966
```
60-
class User < ActiveRecord::Base
61-
attribute :extras, ActiveRecord::Type::Json.new
62-
end
63-
67+
class User < ActiveRecord::Base; end
6468
```
6569

6670
then (ab)use the new attribute!:
@@ -71,6 +75,22 @@ User.create!(login: "saverio", extras: {"uses" => ["mysql", "json"]})
7175
User.last.extras.fetch("uses") # => ["mysql", "json"]
7276
```
7377

78+
The schema can be dumped as usual; json columns will be transparently included:
79+
80+
```sh
81+
$ rake db:schema:dump
82+
$ cat db/schema.rb
83+
ActiveRecord::Schema.define(version: 0) do
84+
85+
create_table "users", force: :cascade do |t|
86+
t.json "extras"
87+
end
88+
89+
end
90+
```
91+
92+
## Caveat/further documentation
93+
7494
Don't forget that JSON doesn't support symbols, therefore, they can be set, but are accessed/loaded as strings.
7595

7696
Users are encouraged to have a look at the test suite ([here](spec/json_on_rails/json_attributes_spec.rb) and [here](spec/json_on_rails/arel_methods_spec.rb)) for an exhaustive view of the functionality.

lib/active_record/connection_adapters/schema_definitions.rb renamed to lib/active_record/connection_adapters/abstract/json_schema_definitions.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
require "active_record/connection_adapters/abstract/schema_definitions"
4+
15
module ActiveRecord
26
# Unfortunately, the type shorthands are hardcoded, and the code itself is not encapsulated
37
# in a method, so there's not much design freedom.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
require "active_record/connection_adapters/mysql2_adapter"
4+
require "active_record/type/json"
5+
6+
# ActiveRecord resources:
7+
#
8+
# lib/active_record/connection_adapters/mysql2_adapter.rb
9+
# lib/active_record/connection_adapters/abstract_mysql_adapter.rb
10+
# lib/active_record/schema_dumper.rb
11+
#
12+
# and a few other files.
13+
#
14+
module ActiveRecord
15+
module ConnectionHandling
16+
def mysql2_json_connection(config)
17+
config = config.symbolize_keys
18+
19+
config[:username] = "root" if config[:username].nil?
20+
21+
if Mysql2::Client.const_defined? :FOUND_ROWS
22+
config[:flags] = Mysql2::Client::FOUND_ROWS
23+
end
24+
25+
client = Mysql2::Client.new(config)
26+
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
27+
ConnectionAdapters::Mysql2JsonAdapter.new(client, logger, options, config)
28+
rescue Mysql2::Error => error
29+
if error.message.include?("Unknown database")
30+
raise ActiveRecord::NoDatabaseError.new(error.message, error)
31+
else
32+
raise
33+
end
34+
end
35+
end
36+
end
37+
38+
module ActiveRecord
39+
module ConnectionAdapters
40+
class Mysql2JsonAdapter < Mysql2Adapter
41+
ADAPTER_NAME = "Mysql2Json".freeze
42+
43+
def native_database_types
44+
super.merge(json: {name: "json"})
45+
end
46+
47+
protected
48+
49+
def initialize_type_map(m)
50+
super
51+
52+
m.register_type %r{json}i, Type::Json.new
53+
end
54+
end
55+
end
56+
end

lib/active_record/type/json.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# frozen_string_literal: true
22

3-
# Resources:
3+
# ActiveRecord resources:
44
#
5-
# gems/activerecord-4.<version>/lib/active_record/attributes.rb
6-
# gems/activerecord-4.<version>/lib/active_record/type/value.rb
7-
# gems/activerecord-4.<version>/lib/active_record/type/mutable.rb
5+
# lib/active_record/attributes.rb
6+
# lib/active_record/type/value.rb
7+
# lib/active_record/type/mutable.rb
88
#
99
module ActiveRecord
1010
module Type

lib/json_on_rails.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
# frozen_string_literal: true
22

3-
require_relative "active_record/type/json"
4-
require_relative "active_record/connection_adapters/schema_definitions"
5-
6-
module JsonOnRails; end
3+
require_relative "active_record/connection_adapters/abstract/json_schema_definitions"
4+
require_relative "active_record/connection_adapters/mysql2_json_adapter"

spec/json_on_rails/arel_methods_spec.rb

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
expect(found_user).to eql(user)
2020
end
2121

22-
# WATCH OUT! The semantic of passing an Array/Hash in Rails is not what one would think it
23-
# would for a JSON data type. This is independent of this gem.
22+
# WATCH OUT! AREL finders are (appropriately) not overloaded. In order to perform JSON-specific
23+
# comparisons, use the MySQL operators (see above).
2424
#
2525
context "passing collection objects" do
2626
it "should not find the instance, when searching an empty hash instance" do
@@ -29,12 +29,10 @@
2929
expect(found_user).to be(nil)
3030
end
3131

32-
it "should not find the instance, when searching an array instance" do
33-
user.update_attributes!(extras: [1, 2, 3])
32+
it "should raise an error, when searching an array instance" do
33+
error_message = "Invalid data type for JSON serialization: #{1.class} (only Hash/Array/nil supported)"
3434

35-
found_user = User.find_by(extras: [1, 2, 3])
36-
37-
expect(found_user).to be(nil)
35+
expect { User.find_by(extras: [1, 2, 3]) }.to raise_error(ArgumentError, error_message)
3836
end
3937
end
4038
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
describe "Schema dump" do
4+
it "should support the json data type" do
5+
output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, StringIO.new)
6+
7+
expect(output.string).to match(/t.json "extras"/)
8+
end
9+
end

spec/setup/config/database.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
adapter: mysql2
1+
adapter: mysql2_json
22
encoding: utf8
33
database: json_on_rails_test
44
socket: <%= ENV.fetch("MYSQL_SOCKET", "/var/run/mysqld/mysqld.sock") %>

spec/setup/models/user.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
# frozen_string_literal: true
22

3-
class User < ActiveRecord::Base
4-
attribute :extras, ActiveRecord::Type::Json.new
5-
end
3+
class User < ActiveRecord::Base; end

0 commit comments

Comments
 (0)