acts_as_recursive_tree 2.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +1 -0
  5. data/.rubocop_todo.yml +321 -0
  6. data/CHANGELOG.md +17 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +175 -0
  10. data/Rakefile +15 -0
  11. data/acts_as_recursive_tree.gemspec +30 -0
  12. data/lib/acts_as_recursive_tree/acts_macro.rb +28 -0
  13. data/lib/acts_as_recursive_tree/associations.rb +25 -0
  14. data/lib/acts_as_recursive_tree/builders/ancestors.rb +17 -0
  15. data/lib/acts_as_recursive_tree/builders/descendants.rb +9 -0
  16. data/lib/acts_as_recursive_tree/builders/leaves.rb +26 -0
  17. data/lib/acts_as_recursive_tree/builders/relation_builder.rb +121 -0
  18. data/lib/acts_as_recursive_tree/builders.rb +13 -0
  19. data/lib/acts_as_recursive_tree/model.rb +125 -0
  20. data/lib/acts_as_recursive_tree/options/depth_condition.rb +48 -0
  21. data/lib/acts_as_recursive_tree/options/query_options.rb +21 -0
  22. data/lib/acts_as_recursive_tree/options/values.rb +77 -0
  23. data/lib/acts_as_recursive_tree/options.rb +9 -0
  24. data/lib/acts_as_recursive_tree/railtie.rb +9 -0
  25. data/lib/acts_as_recursive_tree/scopes.rb +16 -0
  26. data/lib/acts_as_recursive_tree/version.rb +3 -0
  27. data/lib/acts_as_recursive_tree.rb +14 -0
  28. data/spec/builders_spec.rb +136 -0
  29. data/spec/db/database.rb +22 -0
  30. data/spec/db/database.yml +12 -0
  31. data/spec/db/models.rb +37 -0
  32. data/spec/db/schema.rb +34 -0
  33. data/spec/model/location_spec.rb +55 -0
  34. data/spec/model/node_spec.rb +129 -0
  35. data/spec/model/relation_spec.rb +63 -0
  36. data/spec/spec_helper.rb +119 -0
  37. data/spec/values_spec.rb +86 -0
  38. metadata +182 -0
@@ -0,0 +1,17 @@
1
+ module ActsAsRecursiveTree
2
+ module Builders
3
+ class Ancestors < RelationBuilder
4
+
5
+ def build_join_condition
6
+ travers_loc_table[config.parent_key].eq(base_table[config.primary_key])
7
+ end
8
+
9
+ def get_query_options(_)
10
+ opts = super
11
+ opts.ensure_ordering!
12
+ opts
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module ActsAsRecursiveTree
2
+ module Builders
3
+ class Descendants < RelationBuilder
4
+ def build_join_condition
5
+ base_table[config.parent_key].eq(travers_loc_table[config.primary_key])
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ module ActsAsRecursiveTree
2
+ module Builders
3
+ class Leaves < Descendants
4
+
5
+ def create_select_manger
6
+ select_manager = super
7
+
8
+ select_manager.where(
9
+ travers_loc_table[config.primary_key].not_in(
10
+ travers_loc_table.where(
11
+ travers_loc_table[config.parent_key].not_eq(nil)
12
+ ).project(travers_loc_table[config.parent_key])
13
+ )
14
+ )
15
+ select_manager
16
+
17
+ end
18
+
19
+ def get_query_options(_)
20
+ # do not allow any custom options
21
+ ActsAsRecursiveTree::Options::QueryOptions.new
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,121 @@
1
+ module ActsAsRecursiveTree
2
+ module Builders
3
+ class RelationBuilder
4
+
5
+ def self.build(klass, ids, exclude_ids: false, &block)
6
+ new(klass, ids, exclude_ids: exclude_ids, &block).build
7
+ end
8
+
9
+ attr_reader :klass, :ids, :recursive_temp_table, :travers_loc_table
10
+ attr_reader :query_opts, :without_ids
11
+ mattr_reader(:random) { Random.new }
12
+
13
+ def initialize(klass, ids, exclude_ids: false, &block)
14
+ @klass = klass
15
+ @ids = ActsAsRecursiveTree::Options::Values.create(ids, config)
16
+ @without_ids = exclude_ids
17
+
18
+ @query_opts = get_query_options(block)
19
+
20
+ rand_int = random.rand(1_000_000)
21
+ @recursive_temp_table = Arel::Table.new("recursive_#{klass.table_name}_#{rand_int}_temp")
22
+ @travers_loc_table = Arel::Table.new("traverse_#{rand_int}_loc")
23
+ end
24
+
25
+ def get_query_options(proc)
26
+ opts = ActsAsRecursiveTree::Options::QueryOptions.new
27
+
28
+ proc.call(opts) if proc
29
+
30
+ opts
31
+ end
32
+
33
+ def base_table
34
+ klass.arel_table
35
+ end
36
+
37
+ def config
38
+ klass._recursive_tree_config
39
+ end
40
+
41
+ def build
42
+ final_select_mgr = base_table.join(
43
+ create_select_manger.as(recursive_temp_table.name)
44
+ ).on(
45
+ base_table[config.primary_key].eq(recursive_temp_table[config.primary_key])
46
+ )
47
+
48
+ relation = klass.joins(final_select_mgr.join_sources)
49
+
50
+ relation = apply_except_id(relation)
51
+ relation = apply_depth(relation)
52
+ relation = apply_order(relation)
53
+
54
+ relation
55
+ end
56
+
57
+ def apply_except_id(relation)
58
+ return relation unless without_ids
59
+ relation.where(ids.apply_negated_to(base_table[config.primary_key]))
60
+ end
61
+
62
+ def apply_depth(relation)
63
+ return relation unless query_opts.depth_present?
64
+
65
+ relation.where(query_opts.depth.apply_to(recursive_temp_table[config.depth_column]))
66
+ end
67
+
68
+ def apply_order(relation)
69
+ return relation unless query_opts.ensure_ordering
70
+ relation.order(recursive_temp_table[config.depth_column].asc)
71
+ end
72
+
73
+ def create_select_manger
74
+ travers_loc_table.project(Arel.star).with(:recursive, build_cte_table)
75
+ end
76
+
77
+ def build_cte_table
78
+ Arel::Nodes::As.new(
79
+ travers_loc_table,
80
+ build_base_select.union(build_union_select)
81
+ )
82
+ end
83
+
84
+ def build_base_select
85
+ id_node = base_table[config.primary_key]
86
+
87
+ base_table.where(
88
+ ids.apply_to(id_node)
89
+ ).project(
90
+ id_node,
91
+ base_table[config.parent_key],
92
+ Arel.sql('0').as(config.depth_column.to_s)
93
+ )
94
+ end
95
+
96
+ def build_union_select
97
+ select_manager = base_table.join(travers_loc_table).on(
98
+ build_join_condition
99
+ )
100
+
101
+ # need to use ActiveRecord here for merging relation
102
+ relation = klass.select(
103
+ base_table[config.primary_key],
104
+ base_table[config.parent_key],
105
+ Arel.sql(
106
+ (travers_loc_table[config.depth_column] + 1).to_sql
107
+ ).as(config.depth_column.to_s)
108
+ ).unscope(where: :type).joins(select_manager.join_sources)
109
+
110
+ relation = relation.merge(query_opts.condition) unless query_opts.condition.nil?
111
+ relation = relation.where(config.parent_type_column => klass.to_s) if config.parent_type_column
112
+ relation.arel
113
+ end
114
+
115
+ def build_join_condition
116
+ raise 'not implemented'
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,13 @@
1
+ module ActsAsRecursiveTree
2
+ module Builders
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Values
6
+ autoload :DepthCondition
7
+ autoload :QueryOptions
8
+ autoload :RelationBuilder
9
+ autoload :Descendants
10
+ autoload :Ancestors
11
+ autoload :Leaves
12
+ end
13
+ end
@@ -0,0 +1,125 @@
1
+ module ActsAsRecursiveTree
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ ##
6
+ # Returns list of ancestors, starting from parent until root.
7
+ #
8
+ # subchild1.ancestors # => [child1, root]
9
+ #
10
+ def ancestors(&block)
11
+ base_class.ancestors_of(self, &block)
12
+ end
13
+
14
+ # Returns ancestors and current node itself.
15
+ #
16
+ # subchild1.self_and_ancestors # => [subchild1, child1, root]
17
+ #
18
+ def self_and_ancestors(&block)
19
+ base_class.self_and_ancestors_of(self, &block)
20
+ end
21
+
22
+ ##
23
+ # Returns list of descendants, starting from current node, not including current node.
24
+ #
25
+ # root.descendants # => [child1, child2, subchild1, subchild2, subchild3, subchild4]
26
+ #
27
+ def descendants(&block)
28
+ base_class.descendants_of(self, &block)
29
+ end
30
+
31
+ ##
32
+ # Returns list of descendants, starting from current node, including current node.
33
+ #
34
+ # root.self_and_descendants # => [root, child1, child2, subchild1, subchild2, subchild3, subchild4]
35
+ #
36
+ def self_and_descendants(&block)
37
+ base_class.self_and_descendants_of(self, &block)
38
+ end
39
+
40
+ ##
41
+ # Returns the root node of the tree.
42
+ def root
43
+ self_and_ancestors.where(self._recursive_tree_config.parent_key => nil).first
44
+ end
45
+
46
+ ##
47
+ # Returns all siblings of the current node.
48
+ #
49
+ # subchild1.siblings # => [subchild2]
50
+ def siblings
51
+ self_and_siblings.where.not(id: self.id)
52
+ end
53
+
54
+ ##
55
+ # Returns children (without subchildren) and current node itself.
56
+ #
57
+ # root.self_and_children # => [root, child1]
58
+ def self_and_children
59
+ table = self.class.arel_table
60
+ id = self.attributes[self._recursive_tree_config.primary_key.to_s]
61
+
62
+ base_class.where(
63
+ table[self._recursive_tree_config.primary_key].eq(id).or(
64
+ table[self._recursive_tree_config.parent_key].eq(id)
65
+ )
66
+ )
67
+ end
68
+
69
+ ##
70
+ # Returns all Leaves
71
+ #
72
+ def leaves
73
+ base_class.leaves_of(self)
74
+ end
75
+
76
+
77
+ # Returns true if node has no parent, false otherwise
78
+ #
79
+ # subchild1.root? # => false
80
+ # root.root? # => true
81
+ def root?
82
+ self.attributes[self._recursive_tree_config.parent_key.to_s].blank?
83
+ end
84
+
85
+ # Returns true if node has no children, false otherwise
86
+ #
87
+ # subchild1.leaf? # => true
88
+ # child1.leaf? # => false
89
+ def leaf?
90
+ !children.any?
91
+ end
92
+
93
+ def base_class
94
+ self.class.base_class
95
+ end
96
+
97
+ private :base_class
98
+
99
+ module ClassMethods
100
+ def self_and_ancestors_of(ids, &block)
101
+ Builders::Ancestors.build(self, ids, &block)
102
+ end
103
+
104
+ def ancestors_of(ids, &block)
105
+ Builders::Ancestors.build(self, ids, exclude_ids: true, &block)
106
+ end
107
+
108
+ def roots_of(ids)
109
+ self_and_ancestors_of(ids).roots
110
+ end
111
+
112
+ def self_and_descendants_of(ids, &block)
113
+ Builders::Descendants.build(self, ids, &block)
114
+ end
115
+
116
+ def descendants_of(ids, &block)
117
+ Builders::Descendants.build(self, ids, exclude_ids: true, &block)
118
+ end
119
+
120
+ def leaves_of(ids, &block)
121
+ Builders::Leaves.build(self, ids, &block)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,48 @@
1
+ module ActsAsRecursiveTree
2
+ module Options
3
+ class DepthCondition
4
+
5
+ def ==(other)
6
+ @value = Values.create(other)
7
+ @operation = true
8
+ end
9
+
10
+ def !=(other)
11
+ @value = Values.create(other)
12
+ @operation = false
13
+ end
14
+
15
+ def <(other)
16
+ @value = other
17
+ @operation = :lt
18
+ end
19
+
20
+ def <=(other)
21
+ @value = other
22
+ @operation = :lteq
23
+ end
24
+
25
+ def >(other)
26
+ @value = other
27
+ @operation = :gt
28
+ end
29
+
30
+ def >=(other)
31
+ @value = other
32
+ @operation = :gteq
33
+ end
34
+
35
+ def apply_to(attribute)
36
+ if @value.is_a?(Values::Base)
37
+ if @operation
38
+ @value.apply_to(attribute)
39
+ else
40
+ @value.apply_negated_to(attribute)
41
+ end
42
+ else
43
+ attribute.send(@operation, @value)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,21 @@
1
+ module ActsAsRecursiveTree
2
+ module Options
3
+ class QueryOptions
4
+
5
+ attr_accessor :condition
6
+ attr_reader :ensure_ordering
7
+
8
+ def depth
9
+ @depth ||= DepthCondition.new
10
+ end
11
+
12
+ def ensure_ordering!
13
+ @ensure_ordering = true
14
+ end
15
+
16
+ def depth_present?
17
+ @depth.present?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,77 @@
1
+ module ActsAsRecursiveTree
2
+ module Options
3
+ module Values
4
+ class Base
5
+ attr_reader :value, :config
6
+
7
+ def initialize(value, config)
8
+ @value = value
9
+ @config = config
10
+ end
11
+
12
+ def prepared_value
13
+ value
14
+ end
15
+
16
+ def apply_to(attribute)
17
+
18
+ end
19
+
20
+ def apply_negated_to(attribute)
21
+
22
+ end
23
+ end
24
+
25
+ class SingleValue < Base
26
+ def apply_to(attribute)
27
+ attribute.eq(prepared_value)
28
+ end
29
+
30
+ def apply_negated_to(attribute)
31
+ attribute.not_eq(prepared_value)
32
+ end
33
+ end
34
+
35
+ class ActiveRecord < SingleValue
36
+ def prepared_value
37
+ value.id
38
+ end
39
+ end
40
+
41
+ class MultiValue < Base
42
+ def apply_to(attribute)
43
+ attribute.in(prepared_value)
44
+ end
45
+
46
+ def apply_negated_to(attribute)
47
+ attribute.not_in(prepared_value)
48
+ end
49
+ end
50
+
51
+ class Relation < MultiValue
52
+ def prepared_value
53
+ select_manager = value.arel
54
+ select_manager.projections.clear
55
+ select_manager.project(select_manager.froms.last[config.primary_key])
56
+ end
57
+ end
58
+
59
+ def self.create(value, config = nil)
60
+ klass = case value
61
+ when ::Numeric, ::String
62
+ SingleValue
63
+ when ::ActiveRecord::Relation
64
+ Relation
65
+ when Enumerable
66
+ MultiValue
67
+ when ::ActiveRecord::Base
68
+ ActiveRecord
69
+ else
70
+ raise "#{value.class} is not supported"
71
+ end
72
+
73
+ klass.new(value, config)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,9 @@
1
+ module ActsAsRecursiveTree
2
+ module Options
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Values
6
+ autoload :DepthCondition
7
+ autoload :QueryOptions
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module ActsAsRecursiveTree
2
+ class Railtie < Rails::Railtie
3
+ initializer 'acts_as_recursive_tree.active_record_initializer' do
4
+ ActiveRecord::Base.class_exec do
5
+ extend ActsAsRecursiveTree::ActsMacro
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ module ActsAsRecursiveTree
2
+ module Scopes
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ scope :roots, -> {
7
+ rel = where(_recursive_tree_config.parent_key => nil)
8
+ rel = rel.or(
9
+ where.not(_recursive_tree_config.parent_type_column => self.to_s)
10
+ ) if _recursive_tree_config.parent_type_column
11
+
12
+ rel
13
+ }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module ActsAsRecursiveTree
2
+ VERSION = '2.0.0'.freeze
3
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_support/all'
2
+ require_relative 'acts_as_recursive_tree/railtie' if defined?(Rails)
3
+
4
+ module ActsAsRecursiveTree
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :ActsMacro
8
+ autoload :Model
9
+ autoload :Associations
10
+ autoload :Scopes
11
+ autoload :Version
12
+ autoload :Options
13
+ autoload :Builders
14
+ end
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+
3
+ shared_context 'setup with enforced ordering' do
4
+ let(:ordering) { false }
5
+ include_context 'base_setup' do
6
+ let(:proc) { -> (config) { config.ensure_ordering! } }
7
+ end
8
+ end
9
+
10
+ shared_context 'base_setup' do
11
+ let(:model_id) { 1 }
12
+ let(:model_class) { Node }
13
+ let(:exclude_ids) { false }
14
+ let(:proc) { nil }
15
+ let(:builder) do
16
+ described_class.new(model_class, model_id, exclude_ids: exclude_ids, &proc)
17
+ end
18
+ subject(:query) { builder.build.to_sql }
19
+ end
20
+
21
+ shared_examples 'basic recursive examples' do
22
+ it { is_expected.to start_with "SELECT \"#{model_class.table_name}\".* FROM \"#{model_class.table_name}\"" }
23
+ it { is_expected.to match /WHERE "#{model_class.table_name}"."#{model_class.primary_key}" = #{model_id}/ }
24
+ it { is_expected.to match /WITH RECURSIVE "#{builder.travers_loc_table.name}" AS/ }
25
+ it { is_expected.to match /SELECT "#{model_class.table_name}"."#{model_class.primary_key}", "#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}", 0 AS recursive_depth FROM "#{model_class.table_name}"/ }
26
+ it { is_expected.to match /SELECT "#{model_class.table_name}"."#{model_class.primary_key}", "#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}", \("#{builder.travers_loc_table.name}"."recursive_depth" \+ 1\) AS recursive_depth FROM "#{model_class.table_name}"/ }
27
+ it { is_expected.to match /#{Regexp.escape(builder.travers_loc_table.project(Arel.star).to_sql)}/ }
28
+ it { is_expected.to match /"#{model_class.table_name}"."#{model_class.primary_key}" = "#{builder.recursive_temp_table.name}"."#{model_class.primary_key}"/ }
29
+ end
30
+
31
+ shared_examples 'build recursive query' do
32
+ context 'simple id' do
33
+ context 'with simple class' do
34
+ include_context 'base_setup' do
35
+ let(:model_class) { Node }
36
+ it_behaves_like 'basic recursive examples'
37
+ end
38
+ end
39
+
40
+ context 'with class with different parent key' do
41
+ include_context 'base_setup' do
42
+ let(:model_class) { NodeWithOtherParentKey }
43
+ it_behaves_like 'basic recursive examples'
44
+ end
45
+ end
46
+
47
+ context 'with Subclass' do
48
+ include_context 'base_setup' do
49
+ let(:model_class) { Floor }
50
+ it_behaves_like 'basic recursive examples'
51
+ end
52
+ end
53
+
54
+ context 'with polymorphic parent relation' do
55
+ include_context 'base_setup' do
56
+ let(:model_class) { NodeWithPolymorphicParent }
57
+ it_behaves_like 'basic recursive examples'
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ shared_examples 'ancestor query' do
64
+ include_context 'base_setup'
65
+
66
+ it { is_expected.to match /"#{builder.travers_loc_table.name}"."#{model_class._recursive_tree_config.parent_key}" = "#{model_class.table_name}"."#{model_class.primary_key}"/ }
67
+ end
68
+
69
+ shared_examples 'descendant query' do
70
+ include_context 'base_setup'
71
+
72
+ it { is_expected.to match /"#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}" = "#{builder.travers_loc_table.name}"."#{model_class.primary_key}"/ }
73
+ end
74
+
75
+ shared_context 'context with ordering' do
76
+ include_context 'base_setup' do
77
+ it_behaves_like 'with ordering'
78
+ end
79
+ end
80
+
81
+ shared_context 'context without ordering' do
82
+ include_context 'base_setup' do
83
+ it_behaves_like 'without ordering'
84
+ end
85
+ end
86
+
87
+ shared_examples 'with ordering' do
88
+ it { is_expected.to match /ORDER BY #{Regexp.escape(builder.recursive_temp_table[model_class._recursive_tree_config.depth_column].asc.to_sql)}/ }
89
+ end
90
+
91
+ shared_examples 'without ordering' do
92
+ it { is_expected.to_not match /ORDER BY/ }
93
+ end
94
+
95
+ describe ActsAsRecursiveTree::Builders::Descendants do
96
+ context 'basic' do
97
+ it_behaves_like 'build recursive query'
98
+ it_behaves_like 'descendant query'
99
+ include_context 'context without ordering'
100
+ end
101
+
102
+ context 'with options' do
103
+ include_context 'setup with enforced ordering' do
104
+ let(:ordering) { true }
105
+ it_behaves_like 'with ordering'
106
+ end
107
+ end
108
+ end
109
+
110
+ describe ActsAsRecursiveTree::Builders::Ancestors do
111
+ context 'basic' do
112
+ it_behaves_like 'build recursive query'
113
+ it_behaves_like 'ancestor query'
114
+ include_context 'context with ordering'
115
+ end
116
+ context 'with options' do
117
+ include_context 'setup with enforced ordering' do
118
+ it_behaves_like 'with ordering'
119
+ end
120
+ end
121
+ end
122
+
123
+ describe ActsAsRecursiveTree::Builders::Leaves do
124
+ context 'basic' do
125
+ it_behaves_like 'build recursive query'
126
+ it_behaves_like 'descendant query'
127
+ include_context 'context without ordering'
128
+ end
129
+
130
+ context 'with options' do
131
+ include_context 'setup with enforced ordering' do
132
+ let(:ordering) { true }
133
+ it_behaves_like 'without ordering'
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,22 @@
1
+ database_folder = "#{File.dirname(__FILE__)}/../db"
2
+ database_adapter = 'sqlite'
3
+
4
+ # Logger setup
5
+ ActiveRecord::Base.logger = nil
6
+
7
+ ActiveRecord::Migration.verbose = false
8
+
9
+ ActiveRecord::Base.configurations = YAML::load(File.read("#{database_folder}/database.yml"))
10
+
11
+ config = ActiveRecord::Base.configurations[database_adapter]
12
+
13
+ # remove database if present
14
+ FileUtils.rm config['database'], force: true
15
+
16
+ ActiveRecord::Base.establish_connection(database_adapter.to_sym)
17
+ ActiveRecord::Base.establish_connection(config)
18
+
19
+ # require schemata and models
20
+ require_relative 'schema'
21
+ require_relative 'models'
22
+
@@ -0,0 +1,12 @@
1
+ common: &common
2
+ database:
3
+ host: localhost
4
+ pool: 50
5
+ timeout: 5000
6
+ reaping_frequency: 1000
7
+ min_messages: ERROR
8
+
9
+ sqlite:
10
+ <<: *common
11
+ adapter: sqlite3
12
+ database: test.sqlite3