mimi-db 0.1.4 → 0.2.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.
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