mimi-db 0.1.4 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 769933b1c8f1c8f8cc063866b542ad18bd3d1605
4
- data.tar.gz: b84ebf567414894a5c4c60a1c9d0cb8632773def
3
+ metadata.gz: 3b4f245675fc0702efca73d4a00440f13b126907
4
+ data.tar.gz: 04e158a27a77c427e7d56e43c1425ca341254b41
5
5
  SHA512:
6
- metadata.gz: f4fbec564e7394baa9ce38d24feddc6f30a97238d0dde840a47e0481b9dbc772d0a5f36d387ff4f2fcfe17745167a84ad8b038af236ddeb72d19488c19a578f0
7
- data.tar.gz: 6652bd6caa42bef616f936a7752f92e8c6e65877e37aa460dedfbb049b28c8f51b6ca0b09e74cc987e5cba6dd8ced6f7a75c4c6f222a10e20144a9b86295c625
6
+ metadata.gz: f165f27d0c9edd6f27918437337e47f0c47c58ccce22ca450ae121d793765f8407a6889c0ef8d02c13b7d2a5064a19bf2fd2f52f34188f0299e5330809ee1e1c
7
+ data.tar.gz: 70f9f6bf7bbba4e3d4e3ae6f6308ccf0e1ca24adb53c74cbe85e85935e3bb500d832e4e958611b6c3346af715204421a57f29cd4cae6f34211f47bf7b7a1979a
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /vendor/
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mimi/db'
4
+ require 'mimi/logger'
5
+
6
+ CONFIG = {
7
+ db_adapter: 'sqlite3',
8
+ db_database: '../tmp/my_app_db',
9
+ db_log_level: :debug
10
+ }.freeze
11
+
12
+ Mimi::DB.configure(CONFIG)
13
+ Mimi::DB.start
14
+
15
+ class MyModel < ActiveRecord::Base
16
+ field :id, as: :integer, primary_key: true, not_null: true, autoincrement: true
17
+ field :name, as: :string, limit: 64
18
+ field :code, as: :string, default: -> { random_code }
19
+ field :value, as: :decimal, precision: 10, scale: 3
20
+
21
+ index :name
22
+
23
+ def self.random_code
24
+ SecureRandom.hex(16)
25
+ end
26
+ end # class MyModel
27
+
28
+ Mimi::DB.create_if_not_exist! # creates configured database
29
+ Mimi::DB.update_schema!
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mimi
4
+ module DB
5
+ module Dictate
6
+ module DSL
7
+ #
8
+ # Declares a field on a model
9
+ #
10
+ # @example
11
+ # field :id, as: :integer, limit: 8, primary_key: true, default: 'random_uid()'
12
+ #
13
+ # field :name # default type is :string
14
+ # field :value, as: :decimal, precision: 10, scale: 3
15
+ # field :ref_code, as: :string, default: -> { random_ref_code() } # application default
16
+ #
17
+ def field(name, opts = {})
18
+ opts = opts.dup
19
+ # alter model behaviour based on field properties
20
+ if opts[:default].is_a?(Proc)
21
+ field_setup_default(name, opts[:default])
22
+ opts.delete(:default)
23
+ end
24
+
25
+ # register field in the schema
26
+ schema_definition.field(name, opts)
27
+ end
28
+
29
+ # Declares and index on one or several columns
30
+ #
31
+ # @param columns [Symbol,Array<Symbol>] one or several columns
32
+ # @param opts [Hash] index options
33
+ #
34
+ # @example
35
+ # index :name
36
+ # index [:customer_id, :account_id], unique: true, name: 'idx_txs_on_customer_account'
37
+ #
38
+ def index(columns, opts = {})
39
+ schema_definition.index(columns, opts)
40
+ end
41
+
42
+ def schema_definition
43
+ unless self.respond_to?(:table_name)
44
+ raise 'Mimi::DB::Dictate.schema_definition() expects .table_name, not invoked on a Model?'
45
+ end
46
+ Mimi::DB::Dictate.schema_definitions[table_name] ||=
47
+ Mimi::DB::Dictate::SchemaDefinition.new(table_name)
48
+ end
49
+
50
+ private
51
+
52
+ # Sets up a default as a block/Proc
53
+ #
54
+ def field_setup_default(name, block)
55
+ before_validation on: :create do
56
+ self.send :"#{name}=", block.call
57
+ end
58
+ end
59
+ end # module DSL
60
+ end # module Dictate
61
+ end # module DB
62
+ end # module Mimi
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mimi
4
+ module DB
5
+ module Dictate
6
+ module Explorer
7
+ #
8
+ # Discovers a schema of an existing DB table.
9
+ #
10
+ # Returns nil if the DB table does not exist.
11
+ #
12
+ # @param table_name [String]
13
+ # @return [Mimi::DB::Dictate::SchemaDefinition,nil]
14
+ #
15
+ def self.discover_schema(table_name)
16
+ return nil unless connection.tables.include?(table_name)
17
+ sd = Mimi::DB::Dictate::SchemaDefinition.new(table_name)
18
+ discover_schema_columns(sd)
19
+ discover_schema_indexes(sd)
20
+ sd
21
+ end
22
+
23
+ # Discovers columns of an existing DB table and registers them in schema definition
24
+ #
25
+ # @private
26
+ # @param schema_definition [Mimi::DB::Dictate::SchemaDefinition]
27
+ #
28
+ def self.discover_schema_columns(schema_definition)
29
+ columns = connection.columns(schema_definition.table_name)
30
+ pk = connection.primary_key(schema_definition.table_name)
31
+ columns.each do |c|
32
+ params = {
33
+ as: c.type,
34
+ limit: c.limit,
35
+ primary_key: (pk == c.name),
36
+ auto_increment: false, # FIXME: SQLite does not report autoincremented fields
37
+ not_null: !c.null,
38
+ default: c.default,
39
+ sql_type: c.sql_type
40
+ }
41
+ schema_definition.field(c.name, params)
42
+ end
43
+ end
44
+ private_class_method :discover_schema_columns
45
+
46
+ # Discovers indexes of an existing DB table and registers them in schema definition
47
+ #
48
+ # @private
49
+ # @param schema_definition [Mimi::DB::Dictate::SchemaDefinition]
50
+ #
51
+ def self.discover_schema_indexes(schema_definition)
52
+ indexes = connection.indexes(schema_definition.table_name)
53
+ pk = connection.primary_key(schema_definition.table_name)
54
+ indexes.each do |idx|
55
+ params = {
56
+ name: idx.name,
57
+ primary_key: idx.columns == [pk],
58
+ unique: idx.unique
59
+ }
60
+ schema_definition.index(idx.columns, params)
61
+ end
62
+ end
63
+ private_class_method :discover_schema_indexes
64
+
65
+ # Returns ActiveRecord DB connection
66
+ #
67
+ def self.connection
68
+ ActiveRecord::Base.connection
69
+ end
70
+ end # module Explorer
71
+ end # module Dictate
72
+ end # module DB
73
+ end # module Mimi
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mimi
4
+ module DB
5
+ module Dictate
6
+ class Migrator
7
+ DEFAULTS = {
8
+ destructive: {
9
+ tables: false,
10
+ columns: false,
11
+ indexes: false
12
+ },
13
+ dry_run: false,
14
+ logger: nil # will use ActiveRecord::Base.logger
15
+ }.freeze
16
+
17
+ attr_reader :table_name, :options, :from_schema, :to_schema
18
+
19
+ # Creates a migrator to update table schema from DB state to defined state
20
+ #
21
+ # @param table_name [String,Symbol] table name
22
+ # @param options [Hash]
23
+ #
24
+ def initialize(table_name, options)
25
+ @table_name = table_name
26
+ @options = DEFAULTS.merge(options.dup)
27
+ @from_schema = self.class.db_schema_definition(table_name.to_s)
28
+ @to_schema = Mimi::DB::Dictate.schema_definitions[table_name.to_s]
29
+ if from_schema.nil? && to_schema.nil?
30
+ raise "Failed to migrate '#{table_name}', no DB or target schema found"
31
+ end
32
+ end
33
+
34
+ def logger
35
+ @logger ||= options[:logger] || ActiveRecord::Base.logger
36
+ end
37
+
38
+ def db_connection
39
+ ActiveRecord::Base.connection
40
+ end
41
+
42
+ # Returns true if the Migrator is configured to do a dry run (no actual changes to DB)
43
+ #
44
+ def dry_run?
45
+ options[:dry_run]
46
+ end
47
+
48
+ # Returns true if the Migrator is permitted to do destructive operations (DROP ...)
49
+ # on resources identified by :key
50
+ #
51
+ def destructive?(key)
52
+ options[:destructive] == true ||
53
+ (options[:destructive].is_a?(Hash) && options[:destructive][key])
54
+ end
55
+
56
+ def run!
57
+ run_drop_table! if from_schema && to_schema.nil?
58
+ run_change_table! if from_schema && to_schema
59
+ run_create_table! if from_schema.nil? && to_schema
60
+ self.class.reset_db_schema_definition!(table_name)
61
+ end
62
+
63
+ def self.db_schema_definition(table_name)
64
+ db_schema_definitions[table_name] ||=
65
+ Mimi::DB::Dictate::Explorer.discover_schema(table_name)
66
+ end
67
+
68
+ def self.db_schema_definitions
69
+ @db_schema_definitions ||= {}
70
+ end
71
+
72
+ def self.reset_db_schema_definition!(table_name)
73
+ db_schema_definitions[table_name] = nil
74
+ end
75
+
76
+ private
77
+
78
+ def run_drop_table!
79
+ logger.info "- DROP TABLE: #{table_name}"
80
+ return if dry_run? || !destructive?(:tables)
81
+ db_connection.drop_table(table_name)
82
+ end
83
+
84
+ def run_change_table!
85
+ diff = Mimi::DB::Dictate::SchemaDiff.diff(from_schema, to_schema)
86
+ if diff.empty?
87
+ logger.info "- no changes: #{table_name}"
88
+ return
89
+ end
90
+ logger.info "- ALTER TABLE: #{table_name}"
91
+ run_change_table_columns!(diff[:columns]) if diff[:columns]
92
+ run_change_table_indexes!(diff[:indexes]) if diff[:indexes]
93
+ end
94
+
95
+ def run_change_table_columns!(diff_columns)
96
+ diff_columns[:remove]&.each { |c| drop_column!(table_name, c) }
97
+ diff_columns[:change]&.each { |c| change_column!(table_name, c) }
98
+ diff_columns[:add]&.each { |c| add_column!(table_name, c) }
99
+ end
100
+
101
+ def run_change_table_indexes!(diff_indexes)
102
+ diff_indexes[:remove]&.each { |i| drop_index!(table_name, i) }
103
+ diff_indexes[:add]&.each { |i| add_index!(table_name, i) }
104
+ end
105
+
106
+ def run_create_table!
107
+ columns = to_schema.columns.values
108
+ column_pk = to_schema.primary_key
109
+ params =
110
+ column_pk.params.select { |_, v| v }.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
111
+
112
+ # issue CREATE TABLE with primary key field
113
+ logger.info "- CREATE TABLE: #{table_name}"
114
+ logger.info "-- add column: #{table_name}.#{column_pk.name} (#{params})"
115
+ unless dry_run?
116
+ db_connection.create_table(table_name, id: false) do |t|
117
+ t.column column_pk.name, column_pk.type, column_pk.params
118
+ end
119
+ end
120
+
121
+ # create rest of the columns and indexes
122
+ (columns - [column_pk]).each { |c| add_column!(table_name, c) }
123
+ to_schema.indexes.each { |i| add_index!(table_name, i) }
124
+ end
125
+
126
+ def drop_column!(table_name, column_name)
127
+ logger.info "-- drop column: #{table_name}.#{column_name}"
128
+ return if dry_run? || !destructive?(:columns)
129
+ db_connection.remove_column(table_name, column_name)
130
+ end
131
+
132
+ def change_column!(table_name, column)
133
+ params = column.params.select { |_, v| v }.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
134
+ logger.info "-- change column: #{table_name}.#{column.name} (#{params})"
135
+ return if dry_run?
136
+ db_connection.change_column(table_name, column.name, column.type, column.params)
137
+ end
138
+
139
+ def add_column!(table_name, column)
140
+ params = column.params.select { |_, v| v }.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
141
+ logger.info "-- add column: #{table_name}.#{column.name} (#{params})"
142
+ return if dry_run?
143
+ db_connection.add_column(table_name, column.name, column.type, column.params)
144
+ end
145
+
146
+ def drop_index!(table_name, idx)
147
+ idx_column_names = idx.columns.join(', ')
148
+ logger.info "-- drop index: #{idx.name} on #{table_name}(#{idx_column_names})"
149
+ return if dry_run?
150
+ db_connection.remove_index(table_name, column: idx.columns)
151
+ end
152
+
153
+ def add_index!(table_name, idx)
154
+ params = idx.params.select { |_, v| v }.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
155
+ idx_column_names = idx.columns.join(', ')
156
+ logger.info "-- add index: #{idx.name} on #{table_name}(#{idx_column_names}), #{params}"
157
+ return if dry_run?
158
+ db_connection.add_index(table_name, idx.columns, idx.params)
159
+ end
160
+ end # class Migrator
161
+ end # module Dictate
162
+ end # module DB
163
+ end # module Mimi
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mimi
4
+ module DB
5
+ module Dictate
6
+ class SchemaDefinition
7
+ attr_reader :table_name, :columns, :indexes
8
+
9
+ def initialize(table_name)
10
+ @table_name = table_name
11
+ @columns = {}
12
+ @indexes = []
13
+ end
14
+
15
+ def field(name, opts)
16
+ name = name.to_s
17
+ raise "Cannot redefine field :#{name}" if @columns[name]
18
+ if primary_key && (opts[:primary_key] || opts[:as] == :primary_key)
19
+ raise "Cannot redefine primary key (:#{primary_key.name}) with :#{name}"
20
+ end
21
+ @columns[name] = Column.new(name, opts)
22
+ end
23
+
24
+ def index(columns, opts)
25
+ case columns
26
+ when String, Symbol
27
+ columns = [columns.to_s]
28
+ when Array
29
+ unless columns.all? { |c| c.is_a?(String) || c.is_a?(Symbol) }
30
+ raise "Invalid column reference in index definition [#{columns}]"
31
+ end
32
+ columns = columns.map(&:to_s)
33
+ else
34
+ raise 'Invalid columns argument to .index'
35
+ end
36
+ if columns == [primary_key]
37
+ # TODO: warn the primary key index is ignored
38
+ end
39
+ @indexes << Index.new(columns, opts)
40
+ end
41
+
42
+ # Returns primary key column
43
+ #
44
+ # @return [Mimi::DB::Dictate::SchemaDefinition::Column]
45
+ #
46
+ def primary_key
47
+ pk = columns.values.find { |c| c.params[:primary_key] }
48
+ # raise "Primary key is not defined on '#{table_name}'" unless pk
49
+ pk
50
+ end
51
+
52
+ def to_h
53
+ {
54
+ table_name: table_name,
55
+ columns: columns.values.map(&:to_h),
56
+ indexes: indexes.map(&:to_h)
57
+ }
58
+ end
59
+
60
+ class Column
61
+ DEFAULTS = {
62
+ as: :string,
63
+ limit: nil,
64
+ primary_key: false,
65
+ auto_increment: false,
66
+ not_null: false,
67
+ default: nil,
68
+ sql_type: nil
69
+ }.freeze
70
+
71
+ attr_reader :name, :type, :params
72
+
73
+ def initialize(name, opts)
74
+ @name = name
75
+ @type = opts[:as] || DEFAULTS[:as]
76
+ type_defaults = Mimi::DB::Dictate.type_defaults(type)
77
+ @params = DEFAULTS.merge(type_defaults).merge(opts)
78
+ end
79
+
80
+ def to_h
81
+ {
82
+ name: name,
83
+ params: params.dup
84
+ }
85
+ end
86
+
87
+ def ==(other)
88
+ unless other.name == name
89
+ raise ArgumentError, 'Cannot compare columns with different names'
90
+ end
91
+ equal = true
92
+ equal &&= params[:as] == other.params[:as]
93
+ equal &&= params[:limit] == other.params[:limit]
94
+ equal &&= params[:primary_key] == other.params[:primary_key]
95
+ # FIXME: auto_increment ignored
96
+ equal &&= params[:not_null] == other.params[:not_null]
97
+ equal &&= params[:default] == other.params[:default]
98
+ # FIXME: sql_type ignored
99
+ equal
100
+ end
101
+ end # class Column
102
+
103
+ class Index
104
+ DEFAULTS = {
105
+ unique: false
106
+ }.freeze
107
+
108
+ attr_reader :name, :columns, :params
109
+
110
+ def initialize(columns, params)
111
+ @name = params[:name]
112
+ @columns = columns
113
+ @params = DEFAULTS.merge(params)
114
+ end
115
+
116
+ def to_h
117
+ {
118
+ name: name,
119
+ columns: columns,
120
+ params: params.dup
121
+ }
122
+ end
123
+ end # class Index
124
+ end # class SchemaDefinition
125
+ end # module Dictate
126
+ end # module DB
127
+ end # module Mimi
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mimi
4
+ module DB
5
+ module Dictate
6
+ module SchemaDiff
7
+ DEFAULT_OPTIONS = {
8
+ # Force updates on fields defined as :primary_key type.
9
+ # If disabled, the field definition will only be used in 'CREATE TABLE'.
10
+ # (forced updates on :primary_key break on Postgres, at least)
11
+ #
12
+ force_primary_key: false
13
+ }.freeze
14
+
15
+ #
16
+ # Compares two schema definitions
17
+ #
18
+ # @return [Hash] :columns, :indexes => :add, :remove, :change
19
+ #
20
+ def self.diff(from, to, opts = {})
21
+ options = DEFAULT_OPTIONS.merge(opts)
22
+ from_column_names = from.columns.values.map(&:name)
23
+ to_column_names = to.columns.values.map(&:name)
24
+ columns_names_remove = from_column_names - to_column_names
25
+ columns_names_add = to_column_names - from_column_names
26
+ columns_add = to.columns.values.select do |c|
27
+ columns_names_add.include?(c.name)
28
+ end
29
+ columns_change = to.columns.values.reject do |c|
30
+ res = from.columns[c.name].nil? || from.columns[c.name] == c
31
+ res ||= c.type == :primary_key unless options[:force_primary_key]
32
+ end
33
+ from_indexes_c = from.indexes.map(&:columns).uniq
34
+ to_indexes_c = to.indexes.map(&:columns).uniq
35
+ # ignore primary key indexes
36
+ from_indexes_c -= [[from.primary_key&.name]]
37
+ to_indexes_c -= [[to.primary_key&.name]]
38
+
39
+ indexes_c_remove = from_indexes_c - to_indexes_c
40
+ indexes_c_add = to_indexes_c - from_indexes_c
41
+ indexes_remove = from.indexes.select do |idx|
42
+ indexes_c_remove.include?(idx.columns)
43
+ end
44
+ indexes_add = to.indexes.select do |idx|
45
+ indexes_c_add.include?(idx.columns)
46
+ end
47
+
48
+ diff = {}
49
+ unless columns_names_remove.empty?
50
+ diff[:columns] ||= {}
51
+ diff[:columns][:remove] = columns_names_remove
52
+ end
53
+ unless columns_change.empty?
54
+ diff[:columns] ||= {}
55
+ diff[:columns][:change] = columns_change
56
+ end
57
+ unless columns_add.empty?
58
+ diff[:columns] ||= {}
59
+ diff[:columns][:add] = columns_add
60
+ end
61
+ unless indexes_remove.empty?
62
+ diff[:indexes] ||= {}
63
+ diff[:indexes][:remove] = indexes_remove
64
+ end
65
+ unless indexes_add.empty?
66
+ diff[:indexes] ||= {}
67
+ diff[:indexes][:add] = indexes_add
68
+ end
69
+ diff
70
+ end
71
+ end # module SchemaDiff
72
+ end # module Dictate
73
+ end # module DB
74
+ end # module Mimi
@@ -0,0 +1,90 @@
1
+ require_relative 'dictate/dsl'
2
+ require_relative 'dictate/schema_definition'
3
+ require_relative 'dictate/schema_diff'
4
+ require_relative 'dictate/explorer'
5
+ require_relative 'dictate/migrator'
6
+
7
+ module Mimi
8
+ module DB
9
+ module Dictate
10
+ TYPE_DEFAULTS = {
11
+ # sqlite3: {
12
+ # string: { name: 'varchar', limit: 32 }
13
+ # }
14
+ }.freeze
15
+
16
+ def self.included(base)
17
+ base.extend Mimi::DB::Dictate::DSL
18
+ end
19
+
20
+ def self.start
21
+ ActiveRecord::Base.extend Mimi::DB::Dictate::DSL
22
+ end
23
+
24
+ def self.schema_definitions
25
+ @schema_definitions ||= {}
26
+ end
27
+
28
+ def self.adapter_type
29
+ ca = ActiveRecord::ConnectionAdapters
30
+ c = ActiveRecord::Base.connection
31
+
32
+ # TODO: postgres???
33
+ return :cockroachdb if ca.const_defined?(:CockroachDBAdapter) && c.is_a?(ca::PostgreSQLAdapter)
34
+
35
+ return :postgresql if ca.const_defined?(:PostgreSQLAdapter) && c.is_a?(ca::PostgreSQLAdapter)
36
+
37
+ return :mysql if ca.const_defined?(:AbstractMysqlAdapter) && c.is_a?(ca::AbstractMysqlAdapter)
38
+
39
+ return :sqlite3 if ca.const_defined?(:SQLite3Adapter) && c.is_a?(ca::SQLite3Adapter)
40
+
41
+ raise 'Unrecognized database adapter type'
42
+ end
43
+
44
+ # Returns type defaults based on given type:
45
+ # :string
46
+ # :text
47
+ # :integer etc
48
+ #
49
+ def self.type_defaults(type)
50
+ type = type.to_sym
51
+ connection_defaults = ActiveRecord::Base.connection.native_database_types
52
+ adapter_defaults = TYPE_DEFAULTS[DB::Dictate.adapter_type]
53
+ d = (adapter_defaults && adapter_defaults[type]) || connection_defaults[type] || {}
54
+ d = {
55
+ sql_type: d.is_a?(String) ? d : d[:name],
56
+ limit: d.is_a?(String) ? nil : d[:limit]
57
+ }
58
+ if type == :primary_key
59
+ d[:primary_key] = true
60
+ d[:not_null] = true
61
+ end
62
+ d
63
+ end
64
+
65
+ # Updates the DB schema to the target schema defined in models
66
+ #
67
+ # Default options from Migrator::DEFAULTS:
68
+ # destructive: {
69
+ # tables: false,
70
+ # columns: false,
71
+ # indexes: false
72
+ # },
73
+ # dry_run: false,
74
+ # logger: nil # will use ActiveRecord::Base.logger
75
+ #
76
+ # @param opts [Hash]
77
+ #
78
+ def self.update_schema!(opts = {})
79
+ logger = opts[:logger] || ActiveRecord::Base.logger
80
+ logger.info "Mimi::DB::Dictate started updating DB schema"
81
+ t_start = Time.now
82
+ Mimi::DB.all_table_names.each { |t| Mimi::DB::Dictate::Migrator.new(t, opts).run! }
83
+ logger.info 'Mimi::DB::Dictate finished updating DB schema (%.3fs)' % [Time.now - t_start]
84
+ rescue StandardError => e
85
+ logger.error "DB::Dictate failed to update DB schema: #{e}"
86
+ raise
87
+ end
88
+ end # module Dictate
89
+ end # module DB
90
+ end # module Mimi
@@ -2,24 +2,16 @@ module Mimi
2
2
  module DB
3
3
  module Extensions
4
4
  def self.start
5
- install_bigint_primary_keys!
6
- install_bigint_foreign_keys!
7
- end
8
-
9
- def self.install_bigint_primary_keys!
10
- ca = ActiveRecord::ConnectionAdapters
11
-
12
- if ca.const_defined? :PostgreSQLAdapter
13
- ca::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:primary_key] = 'bigserial primary key'
14
- end
5
+ # install DB::Dictate
6
+ ActiveRecord::Base.send(:include, Mimi::DB::Dictate)
15
7
 
16
- if ca.const_defined? :AbstractMysqlAdapter
17
- ca::AbstractMysqlAdapter::NATIVE_DATABASE_TYPES[:primary_key].gsub!(/int\(11\)/, 'bigint')
18
- end
8
+ # FIXME: refactor DSL for primary/foreign keys
9
+ # install_primary_keys!
10
+ # install_bigint_foreign_keys!
19
11
  end
20
12
 
21
13
  def self.install_bigint_foreign_keys!
22
- ActiveRecord::Base.send(:include, Mimi::DB::ForeignKey)
14
+ # ActiveRecord::Base.send(:include, Mimi::DB::ForeignKey)
23
15
  end
24
16
  end # module Extensions
25
17
  end # module DB
@@ -8,18 +8,23 @@ module Mimi
8
8
  # Explicitly specify a (bigint) foreign key
9
9
  #
10
10
  def foreign_key(name, opts = {})
11
- opts = { as: :integer, limit: 8 }.merge(opts)
12
- field(name, opts)
13
- index(name)
11
+ # TODO: refactor and re-implement
12
+ raise 'Not implemented'
13
+
14
+ # opts = { as: :integer, limit: 8 }.merge(opts)
15
+ # field(name, opts)
16
+ # index(name)
14
17
  end
15
18
 
16
19
  # Redefines .belongs_to() with explicitly specified .foreign_key
17
20
  #
18
- def belongs_to(name, opts = {})
19
- foreign_key(:"#{name}_id")
20
- # orig_belongs_to(name, opts)
21
- super
22
- end
21
+ # TODO: refactor and re-implement
22
+ #
23
+ # def belongs_to(name, opts = {})
24
+ # foreign_key(:"#{name}_id")
25
+ # # orig_belongs_to(name, opts)
26
+ # super
27
+ # end
23
28
  end
24
29
  end # module ForeignKey
25
30
  end # module DB
@@ -10,31 +10,123 @@ module Mimi
10
10
  ActiveRecord::Base.descendants
11
11
  end
12
12
 
13
- # Migrates the schema for known models
13
+ # Returns a list of table names defined in models
14
14
  #
15
- def migrate_schema!
16
- models.each(&:auto_upgrade!)
15
+ # @return [Array<String>]
16
+ #
17
+ def model_table_names
18
+ models.map(&:table_name).uniq
19
+ end
20
+
21
+ # Returns a list of all DB table names
22
+ #
23
+ # @return [Array<String>]
24
+ #
25
+ def db_table_names
26
+ ActiveRecord::Base.connection.tables
27
+ end
28
+
29
+ # Returns a list of all discovered table names,
30
+ # both defined in models and existing in DB
31
+ #
32
+ # @return [Array<String>]
33
+ #
34
+ def all_table_names
35
+ (model_table_names + db_table_names).uniq
36
+ end
37
+
38
+ # Updates the DB schema.
39
+ #
40
+ # Brings DB schema to a state defined in models.
41
+ #
42
+ # Default options from Migrator::DEFAULTS:
43
+ # destructive: {
44
+ # tables: false,
45
+ # columns: false,
46
+ # indexes: false
47
+ # },
48
+ # dry_run: false,
49
+ # logger: nil # will use ActiveRecord::Base.logger
50
+ #
51
+ # @example
52
+ # # only detect and report planned changes
53
+ # Mimi::DB.update_schema!(dry_run: true)
54
+ #
55
+ # # modify the DB schema, including all destructive operations
56
+ # Mimi::DB.update_schema!(destructive: true)
57
+ #
58
+ def update_schema!(opts = {})
59
+ opts[:logger] ||= Mimi::DB.logger
60
+ Mimi::DB::Dictate.update_schema!(opts)
17
61
  end
18
62
 
19
63
  # Creates the database specified in the current configuration.
20
64
  #
21
65
  def create!
66
+ db_adapter = Mimi::DB.active_record_config['adapter']
67
+ db_database = Mimi::DB.active_record_config['database']
68
+ slim_url = "#{db_adapter}//<host>:<port>/#{db_database}"
69
+ Mimi::DB.logger.info "Mimi::DB.create! creating database: #{slim_url}"
70
+ original_stdout = $stdout
71
+ original_stderr = $stderr
72
+ $stdout = StringIO.new
73
+ $stderr = StringIO.new
22
74
  ActiveRecord::Tasks::DatabaseTasks.root = Mimi.app_root_path
23
75
  ActiveRecord::Tasks::DatabaseTasks.create(Mimi::DB.active_record_config)
76
+ Mimi::DB.logger.debug "Mimi::DB.create! out:#{$stdout.string}, err:#{$stderr.string}"
77
+ ensure
78
+ $stdout = original_stdout
79
+ $stderr = original_stderr
80
+ end
81
+
82
+ # Tries to establish connection, returns true if the database exist
83
+ #
84
+ def database_exist?
85
+ ActiveRecord::Base.establish_connection(Mimi::DB.active_record_config)
86
+ ActiveRecord::Base.connection
87
+ true
88
+ rescue ActiveRecord::NoDatabaseError
89
+ false
90
+ end
91
+
92
+ # Creates the database specified in the current configuration, if it does NOT exist.
93
+ #
94
+ def create_if_not_exist!
95
+ if database_exist?
96
+ Mimi::DB.logger.debug 'Mimi::DB.create_if_not_exist! database exists, skipping...'
97
+ return
98
+ end
99
+ create!
24
100
  end
25
101
 
26
102
  # Drops the database specified in the current configuration.
27
103
  #
28
104
  def drop!
105
+ original_stdout = $stdout
106
+ original_stderr = $stderr
107
+ $stdout = StringIO.new
108
+ $stderr = StringIO.new
29
109
  ActiveRecord::Tasks::DatabaseTasks.root = Mimi.app_root_path
30
110
  ActiveRecord::Tasks::DatabaseTasks.drop(Mimi::DB.active_record_config)
111
+ Mimi::DB.logger.debug "Mimi::DB.drop! out:#{$stdout.string}, err:#{$stderr.string}"
112
+ ensure
113
+ $stdout = original_stdout
114
+ $stderr = original_stderr
31
115
  end
32
116
 
33
117
  # Clears (but not drops) the database specified in the current configuration.
34
118
  #
35
119
  def clear!
120
+ original_stdout = $stdout
121
+ original_stderr = $stderr
122
+ $stdout = StringIO.new
123
+ $stderr = StringIO.new
36
124
  ActiveRecord::Tasks::DatabaseTasks.root = Mimi.app_root_path
37
125
  ActiveRecord::Tasks::DatabaseTasks.purge(Mimi::DB.active_record_config)
126
+ Mimi::DB.logger.debug "Mimi::DB.clear! out:#{$stdout.string}, err:#{$stderr.string}"
127
+ ensure
128
+ $stdout = original_stdout
129
+ $stderr = original_stderr
38
130
  end
39
131
 
40
132
  # Executes raw SQL, with variables interpolation.
@@ -1,5 +1,5 @@
1
1
  module Mimi
2
2
  module DB
3
- VERSION = '0.1.4'.freeze
3
+ VERSION = '0.2.1'.freeze
4
4
  end
5
5
  end
data/lib/mimi/db.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'mimi/core'
2
2
  require 'active_record'
3
- require 'mini_record'
4
3
 
5
4
  module Mimi
6
5
  module DB
@@ -15,7 +14,11 @@ module Mimi
15
14
  db_username: nil,
16
15
  db_password: nil,
17
16
  db_log_level: :info,
18
- db_pool: 15
17
+ db_pool: 15,
18
+ db_primary_key_cockroachdb: nil,
19
+ db_primary_key_postgresql: nil,
20
+ db_primary_key_mysql: nil,
21
+ db_primary_key_sqlite3: nil
19
22
  # db_encoding:
20
23
  )
21
24
 
@@ -62,9 +65,12 @@ module Mimi
62
65
 
63
66
  def self.configure(*)
64
67
  super
68
+ ActiveSupport::LogSubscriber.colorize_logging = false
65
69
  ActiveRecord::Base.logger = logger
66
70
  ActiveRecord::Base.configurations = { 'default' => active_record_config }
67
- ActiveRecord::Base.raise_in_transactional_callbacks = true
71
+
72
+ # TODO: test and remove deprectated ...
73
+ # ActiveRecord::Base.raise_in_transactional_callbacks = true
68
74
  end
69
75
 
70
76
  def self.logger
@@ -74,6 +80,7 @@ module Mimi
74
80
  def self.start
75
81
  ActiveRecord::Base.establish_connection(:default)
76
82
  Mimi::DB::Extensions.start
83
+ Mimi::DB::Dictate.start
77
84
  Mimi.require_files(module_options[:require_files]) if module_options[:require_files]
78
85
  super
79
86
  end
@@ -98,3 +105,4 @@ require_relative 'db/version'
98
105
  require_relative 'db/extensions'
99
106
  require_relative 'db/helpers'
100
107
  require_relative 'db/foreign_key'
108
+ require_relative 'db/dictate'
data/lib/tasks/db.rake CHANGED
@@ -49,8 +49,14 @@ namespace :db do
49
49
 
50
50
  desc 'Migrate database (schema only)'
51
51
  task schema: :"db:start" do
52
- logger.info "* Migrating database #{Mimi::DB.module_options[:db_database]}"
53
- Mimi::DB.migrate_schema!
52
+ logger.info "* Updating database schema: #{Mimi::DB.module_options[:db_database]}"
53
+ Mimi::DB.update_schema!(destructive: true)
54
+ end
55
+
56
+ desc 'Migrate database (schema only) (DRY RUN)'
57
+ task schema: :"db:start" do
58
+ logger.info "* Updating database schema (DRY RUN): #{Mimi::DB.module_options[:db_database]}"
59
+ Mimi::DB.update_schema!(destructive: true, dry_run: true)
54
60
  end
55
61
  end
56
62
  end
data/mimi-db.gemspec CHANGED
@@ -27,12 +27,13 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ['lib']
29
29
 
30
- spec.add_dependency 'mimi-core', '~> 0.1'
31
- spec.add_dependency 'activerecord', '~> 4.2'
32
- spec.add_dependency 'mini_record', '~> 0.4'
30
+ spec.add_dependency 'mimi-core', '~> 0.2'
31
+ spec.add_dependency 'mimi-logger', '~> 0.2'
32
+ spec.add_dependency 'activerecord', '~> 5.0'
33
33
 
34
34
  spec.add_development_dependency 'bundler', '~> 1.11'
35
35
  spec.add_development_dependency 'rake', '~> 10.0'
36
36
  spec.add_development_dependency 'rspec', '~> 3.0'
37
37
  spec.add_development_dependency 'pry', '~> 0.10'
38
+ spec.add_development_dependency 'sqlite3'
38
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mimi-db
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Kukushkin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-05-17 00:00:00.000000000 Z
11
+ date: 2018-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mimi-core
@@ -16,42 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.1'
19
+ version: '0.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.1'
26
+ version: '0.2'
27
27
  - !ruby/object:Gem::Dependency
28
- name: activerecord
28
+ name: mimi-logger
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '4.2'
33
+ version: '0.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '4.2'
40
+ version: '0.2'
41
41
  - !ruby/object:Gem::Dependency
42
- name: mini_record
42
+ name: activerecord
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.4'
47
+ version: '5.0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.4'
54
+ version: '5.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0.10'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
111
125
  description: Database module for mimi, microframework for microservices
112
126
  email:
113
127
  - alex@kukushk.in
@@ -125,7 +139,14 @@ files:
125
139
  - Rakefile
126
140
  - bin/console
127
141
  - bin/setup
142
+ - examples/my_app.rb
128
143
  - lib/mimi/db.rb
144
+ - lib/mimi/db/dictate.rb
145
+ - lib/mimi/db/dictate/dsl.rb
146
+ - lib/mimi/db/dictate/explorer.rb
147
+ - lib/mimi/db/dictate/migrator.rb
148
+ - lib/mimi/db/dictate/schema_definition.rb
149
+ - lib/mimi/db/dictate/schema_diff.rb
129
150
  - lib/mimi/db/extensions.rb
130
151
  - lib/mimi/db/foreign_key.rb
131
152
  - lib/mimi/db/helpers.rb
@@ -157,7 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
178
  version: '0'
158
179
  requirements: []
159
180
  rubyforge_project:
160
- rubygems_version: 2.4.5.1
181
+ rubygems_version: 2.6.14.1
161
182
  signing_key:
162
183
  specification_version: 4
163
184
  summary: Database module for mimi, microframework for microservices