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 +4 -4
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/examples/my_app.rb +29 -0
- data/lib/mimi/db/dictate/dsl.rb +62 -0
- data/lib/mimi/db/dictate/explorer.rb +73 -0
- data/lib/mimi/db/dictate/migrator.rb +163 -0
- data/lib/mimi/db/dictate/schema_definition.rb +127 -0
- data/lib/mimi/db/dictate/schema_diff.rb +74 -0
- data/lib/mimi/db/dictate.rb +90 -0
- data/lib/mimi/db/extensions.rb +6 -14
- data/lib/mimi/db/foreign_key.rb +13 -8
- data/lib/mimi/db/helpers.rb +95 -3
- data/lib/mimi/db/version.rb +1 -1
- data/lib/mimi/db.rb +11 -3
- data/lib/tasks/db.rake +8 -2
- data/mimi-db.gemspec +4 -3
- metadata +32 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3b4f245675fc0702efca73d4a00440f13b126907
|
4
|
+
data.tar.gz: 04e158a27a77c427e7d56e43c1425ca341254b41
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f165f27d0c9edd6f27918437337e47f0c47c58ccce22ca450ae121d793765f8407a6889c0ef8d02c13b7d2a5064a19bf2fd2f52f34188f0299e5330809ee1e1c
|
7
|
+
data.tar.gz: 70f9f6bf7bbba4e3d4e3ae6f6308ccf0e1ca24adb53c74cbe85e85935e3bb500d832e4e958611b6c3346af715204421a57f29cd4cae6f34211f47bf7b7a1979a
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/examples/my_app.rb
ADDED
@@ -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
|
data/lib/mimi/db/extensions.rb
CHANGED
@@ -2,24 +2,16 @@ module Mimi
|
|
2
2
|
module DB
|
3
3
|
module Extensions
|
4
4
|
def self.start
|
5
|
-
|
6
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
data/lib/mimi/db/foreign_key.rb
CHANGED
@@ -8,18 +8,23 @@ module Mimi
|
|
8
8
|
# Explicitly specify a (bigint) foreign key
|
9
9
|
#
|
10
10
|
def foreign_key(name, opts = {})
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
data/lib/mimi/db/helpers.rb
CHANGED
@@ -10,31 +10,123 @@ module Mimi
|
|
10
10
|
ActiveRecord::Base.descendants
|
11
11
|
end
|
12
12
|
|
13
|
-
#
|
13
|
+
# Returns a list of table names defined in models
|
14
14
|
#
|
15
|
-
|
16
|
-
|
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.
|
data/lib/mimi/db/version.rb
CHANGED
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
|
-
|
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 "*
|
53
|
-
Mimi::DB.
|
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.
|
31
|
-
spec.add_dependency '
|
32
|
-
spec.add_dependency '
|
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
|
+
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:
|
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.
|
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.
|
26
|
+
version: '0.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: mimi-logger
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
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: '
|
40
|
+
version: '0.2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: activerecord
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0
|
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
|
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.
|
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
|