dbagent 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 120c9311649f81fdc84a37b73f207105c4ad560008750418f0f1ea1e581d3b3f
4
+ data.tar.gz: de68e2c53e6119d7df4099e7ae0aafa7beaed5ad1aeb656c2eca90a0e12d9bd0
5
+ SHA512:
6
+ metadata.gz: 5f7e584a37a4c74612bf15fd95c0218fd6e908fd85b8e913410eaaa7c1c22a360ad71033aeb71dfb72563e68bb0f237659fc45756f5a6542b440ccd4eb5d7590
7
+ data.tar.gz: 6636ba62ddecbd1f39c731b36870b4908a0480d4e3d39da6a495947ba66d12802663c4626ae8d1f964652fce4450ad9a45f2b2162924ff62b4253adf6f8edde7
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ # The MIT Licence
2
+
3
+ Copyright (c) 2019 - Enspirit SPRL (Bernard Lambeau)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # DbAgent, a ruby tool to migrate, spy and seed relational databases
2
+
3
+ DbAgent helps managing a relational database lifecyle through three main tools:
4
+
5
+ * Migrations: powered by [Sequel](http://sequel.jeremyevans.net/), migrate as simply as `rake db:migrate`. Supports both superuser and normal user migrations.
6
+
7
+ * Spy: using [Shemaspy](http://schemaspy.sourceforge.net/), get your database schema browsable at any moment, through a simple web interface.
8
+
9
+ * Seed: maintain, install and flush database content as datasets, organized hierarchically in .json files. Very handy for automated tests, for instance.
10
+
11
+ ## Get started using Docker
12
+
13
+ DbAgent is expected to be used as its Docker agent, available as `enspirit/dbagent`. Simply mount migrations and data folders, and you're ready to go.
14
+
15
+ See the examples folder for details.
16
+
17
+ ## Available environment variables
18
+
19
+ * `DBAGENT_ROOT_FOLDER` Main folder where data, migrations and viewpoints can be found
20
+ * `DBAGENT_LOGLEVEL` Log level to use for dbagent messages (defaults to `WARN`)
21
+ * `DBAGENT_LOGSQL` Low Sequel's SQL queries (defaults to `no`)
22
+ * `DBAGENT_ADAPTER` Sequel's adapter (defaults to `postgres`)
23
+ * `DBAGENT_HOST` Database server host (defaults to `localhost`)
24
+ * `DBAGENT_PORT` Database server port (defaults to `5432`)
25
+ * `DBAGENT_DB` Database name (defaults to `suppliers-and-parts`)
26
+ * `DBAGENT_USER` Database user (defaults to `dbagent`)
27
+ * `DBAGENT_PASSWORD` Database password (defaults to `dbagent`)
28
+ * `DBAGENT_SOCKET` Database server socket (if host/port is not used)
29
+ * `DBAGENT_SUPER_USER` Superuser name (postgres only)
30
+ * `DBAGENT_SUPER_DB` Superuser database (postgres only)
31
+ * `DBAGENT_SUPER_PASSWORD` Superuser password (postgres only)
32
+ * `DBAGENT_VIEWPOINT` Bmg viewpoint (class name) when using db:flush
33
+
34
+ ## Available rake tasks
35
+
36
+ The following rake tasks helps you managing the database. They must typically be executed on the docker container.
37
+
38
+ ```
39
+ rake db:check-seeds # Checks that all seeds can be installed correctly
40
+ rake db:create # Creates an fresh new user & database (USE WITH CARE)
41
+ rake db:drop # Drops the user & database (USE WITH CARE)
42
+ rake db:flush[to] # Flushes the database as a particular data set
43
+ rake db:migrate # Runs migrations on the current database
44
+ rake db:ping # Pings the database, making sure everything's ready for migration
45
+ rake db:rebuild # Rebuilds the database from scratch (USE WITH CARE)
46
+ rake db:repl # Opens a database REPL
47
+ rake db:seed[from] # Seeds the database with a particular data set
48
+ rake db:spy # Dumps the schema documentation into database/schema
49
+ rake db:backup # Makes a database backup to the backups folder
50
+ rake db:restore[match] # Restore the last matching database backup file from backups folder
51
+ rake db:revive # Shortcut for both db:restore and db:migrate
52
+ rake db:tables # List tables with those with fewer dependencies first
53
+ rake db:dependencies[of] # List tables that depend of a given one
54
+ ```
55
+
56
+ ## Available webservices
57
+
58
+ ```
59
+ GET /schema/ # Browser the database schema (requires a former `rake db:spy`)
60
+ POST /seeds/install?id=... # Install a particular dataset, id is the name of a folder in `data` folder
61
+ POST /seeds/flush?id=... # Flushes the current database content as a named dataset
62
+ ```
63
+
64
+ ## Hacking on dbagent
65
+
66
+ ### Installing the library
67
+
68
+ ```
69
+ bundle install
70
+ ```
71
+
72
+ ### Preparing your computer
73
+
74
+ The tests require a valid PostgreSQL installation with the suppliers-and-parts
75
+ database installed. A `dbagent` user would be needed on the PostgreSQL installation
76
+ to bootstrap the process.
77
+
78
+ ```
79
+ sudo su postgres -c 'createuser --createdb dbagent -P'
80
+ ```
81
+
82
+ DbAgent tries to connect to the suppliers-and-parts with a dbagent/dbagent user/password
83
+ pair by default. If you change the database name, user, or password please adapt the
84
+ environment variables accordingly in the commands below.
85
+
86
+ ### Installing the example database
87
+
88
+ ```
89
+ DBAGENT_ROOT_FOLDER=examples/suppliers-and-parts bundle exec rake db:create db:migrate db:seed['base']
90
+ ```
91
+ ### Running test
92
+
93
+ To run the test you need to have `Docker` on your computer.
94
+
95
+ Run:
96
+ ```
97
+ make test
98
+ ```
99
+
100
+ Don't forget to delete created ressources for the tests bun running:
101
+ ```
102
+ make clean
103
+ ```
104
+
105
+ ## Contribute
106
+
107
+ Please use github issues and pull requests for all questions, bug reports,
108
+ and contributions. Don't hesitate to get in touch with us with an early code
109
+ spike if you plan to add non trivial features.
110
+
111
+ ## Licence
112
+
113
+ This software is distributed by Enspirit SRL under a MIT Licence. Please
114
+ contact Bernard Lambeau (blambeau@gmail.com) with any question.
115
+
116
+ Enspirit (https://enspirit.be) and Klaro App (https://klaro.cards) are both
117
+ actively using and contributing to the library.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'path'
2
+
3
+ def shell(*cmds)
4
+ cmd = cmds.join("\n")
5
+ puts cmd
6
+ system cmd
7
+ end
8
+
9
+ #
10
+ # Install all tasks found in tasks folder
11
+ #
12
+ # See .rake files there for complete documentation.
13
+ #
14
+ Dir["tasks/*.rake"].each do |taskfile|
15
+ load taskfile
16
+ end
data/lib/db_agent.rb ADDED
@@ -0,0 +1,80 @@
1
+ require 'path'
2
+ require 'logger'
3
+ require 'sequel'
4
+ require 'sinatra'
5
+ require 'bmg'
6
+ require 'bmg/sequel'
7
+ module DbAgent
8
+
9
+ # Current version of DbAgent
10
+ VERSION = "3.0.0"
11
+
12
+ # Simply checks that a path exists of raise an error
13
+ def self._!(path)
14
+ Path(path).tap do |p|
15
+ raise "Missing #{p.basename}." unless p.exists?
16
+ end
17
+ end
18
+
19
+ # Root folder of the project structure
20
+ ROOT_FOLDER = if ENV['DBAGENT_ROOT_FOLDER']
21
+ _!(ENV['DBAGENT_ROOT_FOLDER'])
22
+ else
23
+ Path.backfind('.[Gemfile]') or raise("Missing Gemfile")
24
+ end
25
+
26
+ # Logger instance to use
27
+ LOGGER = Logger.new(STDOUT)
28
+ LOGGER.level = Logger.const_get(ENV['DBAGENT_LOGLEVEL'] || 'WARN')
29
+
30
+ # What database configuration to use for normal access
31
+ def self.default_config
32
+ cfg = {
33
+ adapter: ENV['DBAGENT_ADAPTER'] || 'postgres',
34
+ port: ENV['DBAGENT_PORT'] || 5432,
35
+ database: ENV['DBAGENT_DB'] || 'suppliers-and-parts',
36
+ user: ENV['DBAGENT_USER'] || 'dbagent',
37
+ password: ENV['DBAGENT_PASSWORD'] || 'dbagent',
38
+ test: false
39
+ }
40
+
41
+ # Favor a socket approach if specified, otherwise fallback to
42
+ # host with default to postgres
43
+ if socket = ENV['DBAGENT_SOCKET']
44
+ cfg[:socket] = socket
45
+ else
46
+ cfg[:host] = ENV['DBAGENT_HOST'] || 'localhost'
47
+ end
48
+
49
+ # Set a logger if explicitly requested
50
+ if ENV['DBAGENT_LOGSQL'] == 'yes'
51
+ cfg[:loggers] = [LOGGER]
52
+ end
53
+
54
+ cfg
55
+ end
56
+
57
+ # What database configuration to use for superuser access
58
+ def self.default_superconfig
59
+ cfg = default_config
60
+ cfg.merge({
61
+ user: ENV['DBAGENT_SUPER_USER'] || cfg[:user],
62
+ database: ENV['DBAGENT_SUPER_DB'] || cfg[:database],
63
+ password: ENV['DBAGENT_SUPER_PASSWORD'] || cfg[:password]
64
+ })
65
+ end
66
+
67
+ def self.default_handler
68
+ DbHandler.factor({
69
+ config: default_config,
70
+ superconfig: default_superconfig,
71
+ root: ROOT_FOLDER
72
+ })
73
+ end
74
+
75
+ end # module DbAgent
76
+ require 'db_agent/viewpoint'
77
+ require 'db_agent/seeder'
78
+ require 'db_agent/table_orderer'
79
+ require 'db_agent/db_handler'
80
+ require 'db_agent/webapp'
@@ -0,0 +1,130 @@
1
+ module DbAgent
2
+ class DbHandler
3
+
4
+ def initialize(options)
5
+ @config = options[:config]
6
+ @superconfig = options[:superconfig]
7
+ @root_folder = options[:root]
8
+ @backup_folder = options[:backup] || options[:root]/'backups'
9
+ @schema_folder = options[:schema] || options[:root]/'schema'
10
+ @migrations_folder = options[:migrations] || options[:root]/'migrations'
11
+ @data_folder = options[:data] || options[:root]/'data'
12
+ @viewpoints_folder = options[:viewpoints] || options[:root]/'viewpoints'
13
+ require_viewpoints!
14
+ end
15
+ attr_reader :config, :superconfig
16
+ attr_reader :backup_folder, :schema_folder, :migrations_folder
17
+ attr_reader :data_folder, :viewpoints_folder
18
+
19
+ def ping
20
+ puts "Using #{config}"
21
+ sequel_db.test_connection
22
+ puts "Everything seems fine!"
23
+ end
24
+
25
+ def create
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def drop
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def backup
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def repl
38
+ raise NotImplementedError
39
+ end
40
+
41
+ def wait_server
42
+ require 'net/ping'
43
+ raise "No host found" unless config[:host]
44
+ check = Net::Ping::External.new(config[:host])
45
+ puts "Trying to ping `#{config[:host]}`"
46
+ 15.downto(0) do |i|
47
+ print "."
48
+ if check.ping?
49
+ print "\nServer found.\n"
50
+ break
51
+ elsif i == 0
52
+ print "\n"
53
+ raise "Server not found, I give up."
54
+ else
55
+ sleep(1)
56
+ end
57
+ end
58
+ end
59
+
60
+ def wait
61
+ 15.downto(0) do |i|
62
+ begin
63
+ puts "Using #{config}"
64
+ sequel_db.test_connection
65
+ puts "Database is there. Great."
66
+ break
67
+ rescue Sequel::Error
68
+ raise if i==0
69
+ sleep(1)
70
+ end
71
+ end
72
+ end
73
+
74
+ def restore(t, args)
75
+ raise NotImplementedError
76
+ end
77
+
78
+ def migrate(version = nil)
79
+ Sequel.extension :migration
80
+ if (sf = migrations_folder/'superuser').exists?
81
+ Sequel::Migrator.run(sequel_superdb, migrations_folder/'superuser', table: 'superuser_migrations', target: version)
82
+ end
83
+ Sequel::Migrator.run(sequel_db, migrations_folder, target: version)
84
+ end
85
+
86
+ def repl
87
+ raise NotImplementedError
88
+ end
89
+
90
+ def spy
91
+ raise NotImplementedError
92
+ end
93
+
94
+ def self.factor(options)
95
+ case options[:config][:adapter]
96
+ when 'postgres'
97
+ PostgreSQL.new(options)
98
+ when 'mssql'
99
+ MSSQL.new(options)
100
+ when 'mysql'
101
+ MySQL.new(options)
102
+ else
103
+ PostgreSQL.new(options)
104
+ end
105
+ end
106
+
107
+ def sequel_db
108
+ @sequel_db ||= ::Sequel.connect(config)
109
+ end
110
+
111
+ def sequel_superdb
112
+ raise "No superconfig set" if superconfig.nil?
113
+ @sequel_superdb ||= ::Sequel.connect(superconfig)
114
+ end
115
+
116
+ def system(cmd, *args)
117
+ puts cmd
118
+ ::Kernel.system(cmd, *args)
119
+ end
120
+
121
+ def require_viewpoints!
122
+ f = viewpoints_folder.expand_path
123
+ Path.require_tree(f) if f.directory?
124
+ end
125
+
126
+ end # class DbHandler
127
+ end # module DbAgent
128
+ require_relative 'db_handler/postgresql'
129
+ require_relative 'db_handler/mssql'
130
+ require_relative 'db_handler/mysql'
@@ -0,0 +1,31 @@
1
+ module DbAgent
2
+ class DbHandler
3
+ class MSSQL < DbHandler
4
+
5
+ def create
6
+ raise
7
+ end
8
+
9
+ def drop
10
+ raise
11
+ end
12
+
13
+ def backup
14
+ raise
15
+ end
16
+
17
+ def repl
18
+ raise
19
+ end
20
+
21
+ def spy
22
+ jdbc_jar = (Path.dir.parent/'vendor').glob('mssql*.jar').first
23
+ system %Q{java -jar vendor/schemaSpy_5.0.0.jar -dp #{jdbc_jar} -t mssql05 -host #{config[:host]} -u #{config[:user]} -p #{config[:password]} -db #{config[:database]} -port #{config[:port]} -s dbo -o #{schema_folder}/spy}
24
+ system %Q{open #{schema_folder}/spy/index.html}
25
+ end
26
+
27
+ def restore(t, args)
28
+ end
29
+ end # MSSQL
30
+ end # DbHandler
31
+ end # DbAgent
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DbAgent
4
+ class DbHandler
5
+ class MySQL < DbHandler
6
+ def create
7
+ raise
8
+ end
9
+
10
+ def drop
11
+ raise
12
+ end
13
+
14
+ def backup
15
+ datetime = Time.now.strftime('%Y%m%dT%H%M%S')
16
+ shell mysqldump(config[:database], "> #{backup_folder}/backup-#{datetime}.sql")
17
+ end
18
+
19
+ def repl
20
+ shell mysql(config[:database])
21
+ end
22
+
23
+ def spy
24
+ jdbc_jar = (Path.dir.parent / 'vendor').glob('mysql*.jar').first
25
+ system %(java -jar vendor/schemaSpy_5.0.0.jar -dp #{jdbc_jar} -t mysql -host #{config[:host]} -u #{config[:user]} -p #{config[:password]} -db #{config[:database]} -s public -o #{schema_folder}/spy)
26
+ system %(open #{schema_folder}/spy/index.html)
27
+ end
28
+
29
+ def restore(_t, args)
30
+ candidates = backup_folder.glob('*.sql').sort
31
+ if args[:pattern] && rx = Regexp.new(args[:pattern])
32
+ candidates = candidates.select { |f| f.basename.to_s =~ rx }
33
+ end
34
+ file = candidates.last
35
+ shell mysql(config[:database], '<', file.to_s)
36
+ end
37
+
38
+ private
39
+
40
+ def mysql_cmd(cmd, *args)
41
+ conf = config
42
+ %(#{cmd} -h #{config[:host]} --password=#{config[:password]} -P #{config[:port]} -u #{config[:user]} #{args.join(' ')})
43
+ end
44
+
45
+ def mysql(*args)
46
+ mysql_cmd('mysql', *args)
47
+ end
48
+
49
+ def mysqldump(*args)
50
+ mysql_cmd('mysqldump', *args)
51
+ end
52
+ end # MySQL
53
+ end # DbHandler
54
+ end # DbAgent
@@ -0,0 +1,66 @@
1
+ module DbAgent
2
+ class DbHandler
3
+ class PostgreSQL < DbHandler
4
+
5
+ def create
6
+ shell pg_cmd("createuser","--no-createdb","--no-createrole","--no-superuser","--no-password",config[:user]),
7
+ pg_cmd("createdb","--owner=#{config[:user]}", config[:database])
8
+ end
9
+
10
+ def drop
11
+ shell pg_cmd("dropdb", config[:database]),
12
+ pg_cmd("dropuser", config[:user])
13
+ end
14
+
15
+ def backup
16
+ datetime = Time.now.strftime("%Y%m%dT%H%M%S")
17
+ shell pg_dump("--clean", config[:database], "> #{backup_folder}/backup-#{datetime}.sql")
18
+ end
19
+
20
+ def repl
21
+ shell pg_cmd('psql', config[:database])
22
+ end
23
+
24
+ def spy
25
+ spy_jar = DbAgent._!('vendor').glob('schema*.jar').first
26
+ jdbc_jar = DbAgent._!('vendor').glob('postgresql*.jar').first
27
+ cmd = ""
28
+ cmd << %Q{java -jar #{spy_jar} -dp #{jdbc_jar} -t pgsql}
29
+ cmd << %Q{ -host #{config[:host]}}
30
+ cmd << %Q{ -port #{config[:port]}} if config[:port]
31
+ cmd << %Q{ -u #{config[:user]}}
32
+ cmd << %Q{ -p #{config[:password]}} if config[:password]
33
+ cmd << %Q{ -db #{config[:database]} -s public -o #{schema_folder}/spy}
34
+ system(cmd)
35
+ system %Q{open #{schema_folder}/spy/index.html}
36
+ end
37
+
38
+ def restore(t, args)
39
+ candidates = backup_folder.glob("*.sql").sort
40
+ if args[:pattern] && rx = Regexp.new(args[:pattern])
41
+ candidates = candidates.select{|f| f.basename.to_s =~ rx }
42
+ end
43
+ file = candidates.last
44
+ shell pg_cmd('psql', config[:database], '<', file.to_s)
45
+ end
46
+
47
+ private
48
+
49
+ def pg_cmd(cmd, *args)
50
+ %Q{#{cmd} -h #{config[:host]} -p #{config[:port]} -U #{config[:user]} #{args.join(' ')}}
51
+ end
52
+
53
+ def psql(*args)
54
+ cmd = "psql"
55
+ cmd = "PGPASSWORD=#{config[:password]} #{cmd}" if config[:password]
56
+ pg_cmd(cmd, *args)
57
+ end
58
+
59
+ def pg_dump(*args)
60
+ cmd = "pg_dump"
61
+ cmd = "PGPASSWORD=#{config[:password]} #{cmd}" if config[:password]
62
+ pg_cmd(cmd, *args)
63
+ end
64
+ end # class PostgreSQL
65
+ end # module DbHandler
66
+ end # module DbAgent
@@ -0,0 +1,125 @@
1
+ module DbAgent
2
+ class Seeder
3
+
4
+ def initialize(handler)
5
+ @handler = handler
6
+ end
7
+ attr_reader :handler
8
+
9
+ def install(from)
10
+ handler.sequel_db.transaction do
11
+ folder = handler.data_folder/from
12
+
13
+ # load files in order
14
+ pairs = merged_data(from)
15
+ names = pairs.keys.sort{|p1,p2|
16
+ pairs[p1].basename <=> pairs[p2].basename
17
+ }
18
+
19
+ # Truncate tables then fill them
20
+ names.reverse.each do |name|
21
+ LOGGER.info("Emptying table `#{name}`")
22
+ handler.sequel_db[name.to_sym].delete
23
+ end
24
+ names.each do |name|
25
+ LOGGER.info("Filling table `#{name}`")
26
+ file = pairs[name]
27
+ handler.sequel_db[name.to_sym].multi_insert(file.load)
28
+ end
29
+ end
30
+ end
31
+
32
+ def flush_empty(to = "empty")
33
+ target = (handler.data_folder/to).rm_rf.mkdir_p
34
+ (target/"metadata.json").write <<-JSON.strip
35
+ {}
36
+ JSON
37
+ TableOrderer.new(handler).tsort.each_with_index do |table_name, index|
38
+ (target/"#{(index*10).to_s.rjust(5,"0")}-#{table_name}.json").write("[]")
39
+ end
40
+ end
41
+
42
+ def flush(to)
43
+ target = (handler.data_folder/to).rm_rf.mkdir_p
44
+ source = (handler.data_folder/"empty")
45
+ (target/"metadata.json").write <<-JSON.strip
46
+ { "inherits": "empty" }
47
+ JSON
48
+ seed_files(source).each do |f|
49
+ flush_seed_file(f, to)
50
+ end
51
+ end
52
+
53
+ def flush_seed_file(f, to)
54
+ target = (handler.data_folder/to)
55
+ table = file2table(f)
56
+ flush_table(table, target, f.basename, true)
57
+ end
58
+
59
+ def flush_table(table_name, target_folder, file_name, skip_empty)
60
+ data = viewpoint.send(table_name.to_sym).to_a
61
+ if data.empty? && skip_empty
62
+ LOGGER.info("Skipping table `#{table_name}` since empty")
63
+ else
64
+ LOGGER.info("Flushing table `#{table_name}`")
65
+ json = JSON.pretty_generate(data)
66
+ (target_folder/file_name).write(json)
67
+ end
68
+ end
69
+
70
+ def each_seed(install = true)
71
+ handler.data_folder.glob('**/*') do |file|
72
+ next unless file.directory?
73
+ next unless (file/"metadata.json").exists?
74
+
75
+ base = file.relative_to(handler.data_folder)
76
+ begin
77
+ Seeder.new(handler).install(base)
78
+ puts "#{base} OK"
79
+ yield(self, file) if block_given?
80
+ rescue => ex
81
+ puts "KO on #{file}"
82
+ puts ex.message
83
+ end if install
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def merged_data(from)
90
+ folder = handler.data_folder/from
91
+ data = {}
92
+
93
+ # load metadata and install parent dataset if any
94
+ metadata = (folder/"metadata.json").load
95
+ if parent = metadata["inherits"]
96
+ data = merged_data(parent)
97
+ end
98
+
99
+ seed_files(folder).each do |f|
100
+ data[file2table(f)] = f
101
+ end
102
+
103
+ data
104
+ end
105
+
106
+ def seed_files(folder)
107
+ folder
108
+ .glob("*.json")
109
+ .reject{|f| f.basename.to_s =~ /^metadata/ }
110
+ end
111
+
112
+ def file2table(f)
113
+ f.basename.rm_ext.to_s[/^\d+-(.*)/, 1]
114
+ end
115
+
116
+ def viewpoint
117
+ @viewpoint ||= if vp = ENV['DBAGENT_VIEWPOINT']
118
+ Kernel.const_get(vp).new(handler.sequel_db)
119
+ else
120
+ Viewpoint::Base.new(handler.sequel_db)
121
+ end
122
+ end
123
+
124
+ end # class Seeder
125
+ end # module DbAgent
@@ -0,0 +1,94 @@
1
+ require 'tsort'
2
+ module DbAgent
3
+ class TableOrderer
4
+
5
+ def initialize(handler)
6
+ @handler = handler
7
+ end
8
+ attr_reader :handler
9
+
10
+ def db
11
+ handler.sequel_db
12
+ end
13
+
14
+ def tsort
15
+ @tsort ||= TSortComputation.new(db).to_a
16
+ end
17
+
18
+ def graph
19
+ @graph ||= TSortComputation.new(db).graph
20
+ end
21
+
22
+ def dependencies(table)
23
+ _dependencies(table, ds = {})
24
+ ds
25
+ .inject([]){|memo,(_,plus)| (memo + plus).uniq }
26
+ .sort{|t1,t2| tsort.index(t1) - tsort.index(t2) }
27
+ .reject{|x| x == table }
28
+ end
29
+
30
+ def _dependencies(table, ds)
31
+ return ds if ds.has_key?(table)
32
+ ds[table] = graph[table]
33
+ ds[table].each do |child|
34
+ _dependencies(child, ds)
35
+ end
36
+ end
37
+ private :_dependencies
38
+
39
+ class TSortComputation
40
+ include TSort
41
+
42
+ def initialize(db, except = [])
43
+ @db = db
44
+ @except = except
45
+ end
46
+ attr_reader :db, :except
47
+
48
+ def graph
49
+ g = Hash.new{|h,k| h[k] = [] }
50
+ tsort_each_node.each do |table|
51
+ tsort_each_child(table) do |child|
52
+ g[child] << table
53
+ end
54
+ end
55
+ g
56
+ rescue TSort::Cyclic
57
+ raise unless killed = to_kill
58
+ TSortComputation.new(db, except + [killed]).graph
59
+ end
60
+
61
+ def to_a
62
+ tsort.to_a
63
+ rescue TSort::Cyclic
64
+ raise unless killed = to_kill
65
+ TSortComputation.new(db, except + [killed]).to_a
66
+ end
67
+
68
+ def tsort_each_node(&bl)
69
+ db.tables.each(&bl)
70
+ end
71
+
72
+ def tsort_each_child(table, &bl)
73
+ db.foreign_key_list(table)
74
+ .map{|fk| fk[:table] }
75
+ .reject{|t|
76
+ except.any?{|killed| killed.first == table && killed.last == t }
77
+ }
78
+ .each(&bl)
79
+ end
80
+
81
+ private
82
+
83
+ def to_kill
84
+ each_strongly_connected_component
85
+ .select{|scc| scc.size > 1 }
86
+ .sort_by{|scc| scc.size }
87
+ .first
88
+ end
89
+
90
+ end # class TSortComputation
91
+
92
+ end # class TableOrderer
93
+ end # module Dbagent
94
+
@@ -0,0 +1,3 @@
1
+ require_relative 'viewpoint/base'
2
+ require_relative 'viewpoint/delegate'
3
+ require_relative 'viewpoint/typecheck'
@@ -0,0 +1,18 @@
1
+ module DbAgent
2
+ module Viewpoint
3
+ # Factors relations on top of a Sequel database.
4
+ class Base
5
+
6
+ def initialize(db)
7
+ @db = db
8
+ end
9
+ attr_reader :db
10
+
11
+ def method_missing(name, *args, &bl)
12
+ return super unless args.empty? and bl.nil?
13
+ Bmg.sequel(name, db)
14
+ end
15
+
16
+ end # class Base
17
+ end # module Viewpoint
18
+ end # module DbAgent
@@ -0,0 +1,13 @@
1
+ module DbAgent
2
+ module Viewpoint
3
+ # Delegates all relation accesses to `child`.
4
+ module Delegate
5
+
6
+ def method_missing(name, *args, &bl)
7
+ return super unless args.empty? and bl.nil?
8
+ child.send(name)
9
+ end
10
+
11
+ end # module Delegate
12
+ end # module Viewpoint
13
+ end # module DbAgent
@@ -0,0 +1,19 @@
1
+ module DbAgent
2
+ module Viewpoint
3
+ # Forces typechecking on all child relations.
4
+ class TypeCheck
5
+
6
+ def initialize(db, child = nil)
7
+ @db = db
8
+ @child = child || DbAgent::Viewpoint::Base.new(db)
9
+ end
10
+ attr_reader :db, :child
11
+
12
+ def method_missing(name, *args, &bl)
13
+ return super unless args.empty? && bl.nil?
14
+ child.send(name).with_typecheck
15
+ end
16
+
17
+ end # class TypeCheck
18
+ end # module Viewpoint
19
+ end # module DbAgent
@@ -0,0 +1,35 @@
1
+ module DbAgent
2
+ class Webapp < Sinatra::Base
3
+
4
+ set :raise_errors, true
5
+ set :show_exceptions, false
6
+ set :dump_errors, true
7
+ set :db_handler, DbAgent.default_handler
8
+
9
+ get '/ping' do
10
+ settings.db_handler.sequel_db.test_connection
11
+ status 200
12
+ "ok"
13
+ end
14
+
15
+ get %r{/schema/?} do
16
+ send_file(settings.db_handler.schema_folder/'spy/index.html')
17
+ end
18
+
19
+ get '/schema/*' do |url|
20
+ send_file(settings.db_handler.schema_folder/'spy'/url)
21
+ end
22
+
23
+ post '/seeds/install' do
24
+ Seeder.new(settings.db_handler.sequel_db).install(request["id"])
25
+ "ok"
26
+ end
27
+
28
+ post '/seeds/flush' do
29
+ seed_name = request["id"]
30
+ Seeder.new(settings.db_handler.sequel_db).flush(request["id"])
31
+ "ok"
32
+ end
33
+
34
+ end
35
+ end
data/tasks/db.rake ADDED
@@ -0,0 +1,108 @@
1
+ namespace :db do
2
+
3
+ task :require do
4
+ $:.unshift File.expand_path('../../lib', __FILE__)
5
+ require 'db_agent'
6
+ include DbAgent
7
+ end
8
+
9
+ def db_handler
10
+ @db_handler ||= DbAgent.default_handler
11
+ end
12
+
13
+ desc "Pings the database, making sure everything's ready for migration"
14
+ task :ping => :require do
15
+ db_handler.ping
16
+ end
17
+
18
+ desc "Drops the user & database (USE WITH CARE)"
19
+ task :drop => :require do
20
+ db_handler.drop
21
+ end
22
+
23
+ desc "Creates an fresh new user & database (USE WITH CARE)"
24
+ task :create => :require do
25
+ db_handler.create
26
+ end
27
+
28
+ desc "Waits for the database server to ping, up to 15 seconds"
29
+ task :wait_server => :require do
30
+ db_handler.wait_server
31
+ end
32
+
33
+ desc "Waits for the database to ping, up to 15 seconds"
34
+ task :wait => :require do
35
+ db_handler.wait
36
+ end
37
+
38
+ desc "Dump a database backup"
39
+ task :backup => :require do
40
+ db_handler.backup
41
+ end
42
+
43
+ desc "Restore from the last database backup"
44
+ task :restore, :pattern do |t,args|
45
+ db_handler.restore(t, args)
46
+ end
47
+ task :restore => :require
48
+
49
+ desc "Runs migrations on the current database"
50
+ task :migrate, [:version] => :require do |_,args|
51
+ version = args[:version].to_i if args[:version]
52
+ db_handler.migrate(version)
53
+ end
54
+
55
+ desc "Opens a database REPL"
56
+ task :repl => :require do
57
+ db_handler.repl
58
+ end
59
+
60
+ desc "Dumps the schema documentation into database/schema"
61
+ task :spy => :require do
62
+ db_handler.spy
63
+ end
64
+
65
+ desc "Rebuilds the database from scratch (USE WITH CARE)"
66
+ task :rebuild => [ :drop, :create, :migrate ]
67
+
68
+ desc "Revive the database from the last backup"
69
+ task :revive => [ :restore, :migrate ]
70
+
71
+
72
+ desc "Checks that all seeds can be installed correctly"
73
+ task :"check-seeds" do
74
+ Seeder.new(db_handler).each_seed(true)
75
+ end
76
+ task :"check-seeds" => :require
77
+
78
+ desc "Seeds the database with a particular data set"
79
+ task :seed, :from do |t,args|
80
+ Seeder.new(db_handler).install(args[:from] || 'empty')
81
+ end
82
+ task :seed => :require
83
+
84
+ desc "Flushes the database as a particular data set"
85
+ task :flush, :to do |t,args|
86
+ Seeder.new(db_handler).flush(args[:to] || Time.now.strftime("%Y%M%d%H%M%S").to_s)
87
+ end
88
+ task :flush => :require
89
+
90
+ desc "Flushes the initial empty files as a data set"
91
+ task :flush_empty, :to do |t,args|
92
+ Seeder.new(db_handler).flush_empty(args[:to] || Time.now.strftime("%Y%M%d%H%M%S").to_s)
93
+ end
94
+ task :flush_empty => :require
95
+
96
+ desc "Shows what tables depend on a given one"
97
+ task :dependencies, :of do |t,args|
98
+ puts TableOrderer.new(db_handler).dependencies(args[:of].to_sym).reverse
99
+ end
100
+ task :dependencies => :require
101
+
102
+ desc "Shows all tables in order"
103
+ task :tables do |t|
104
+ puts TableOrderer.new(db_handler).tsort
105
+ end
106
+ task :tables => :require
107
+
108
+ end
data/tasks/gem.rake ADDED
@@ -0,0 +1,39 @@
1
+ require 'rubygems/package_task'
2
+
3
+ # Dynamically load the gem spec
4
+ gemspec_file = File.expand_path('../../dbagent.gemspec', __FILE__)
5
+ gemspec = Kernel.eval(File.read(gemspec_file))
6
+
7
+ Gem::PackageTask.new(gemspec) do |t|
8
+
9
+ # Name of the package
10
+ t.name = gemspec.name
11
+
12
+ # Version of the package
13
+ t.version = gemspec.version
14
+
15
+ # Directory used to store the package files
16
+ t.package_dir = "pkg"
17
+
18
+ # True if a gzipped tar file (tgz) should be produced
19
+ t.need_tar = false
20
+
21
+ # True if a gzipped tar file (tar.gz) should be produced
22
+ t.need_tar_gz = false
23
+
24
+ # True if a bzip2'd tar file (tar.bz2) should be produced
25
+ t.need_tar_bz2 = false
26
+
27
+ # True if a zip file should be produced (default is false)
28
+ t.need_zip = false
29
+
30
+ # List of files to be included in the package.
31
+ t.package_files = gemspec.files
32
+
33
+ # Tar command for gzipped or bzip2ed archives.
34
+ t.tar_command = "tar"
35
+
36
+ # Zip command for zipped archives.
37
+ t.zip_command = "zip"
38
+
39
+ end
data/tasks/test.rake ADDED
@@ -0,0 +1,11 @@
1
+ namespace :test do
2
+
3
+ desc %q{Run all RSpec tests}
4
+ task :unit do
5
+ require 'rspec'
6
+ RSpec::Core::Runner.run(%w[-I. -Ilib -Ispec --pattern=spec/**/test_*.rb --color .])
7
+ end
8
+
9
+ task :all => :"unit"
10
+ end
11
+ task :test => :"test:all"
metadata ADDED
@@ -0,0 +1,215 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dbagent
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Bernard Lambeau
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-08-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sequel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: path
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sinatra
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bmg
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.18'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.18'
97
+ - !ruby/object:Gem::Dependency
98
+ name: net-ping
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: predicate
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '2'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: bundler
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '2'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '2'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '3'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '3'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rack-test
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '1'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '1'
167
+ description: A tool to migrate, spy and seed relational databases
168
+ email: blambeau@gmail.com
169
+ executables: []
170
+ extensions: []
171
+ extra_rdoc_files: []
172
+ files:
173
+ - Gemfile
174
+ - LICENSE.md
175
+ - README.md
176
+ - Rakefile
177
+ - lib/db_agent.rb
178
+ - lib/db_agent/db_handler.rb
179
+ - lib/db_agent/db_handler/mssql.rb
180
+ - lib/db_agent/db_handler/mysql.rb
181
+ - lib/db_agent/db_handler/postgresql.rb
182
+ - lib/db_agent/seeder.rb
183
+ - lib/db_agent/table_orderer.rb
184
+ - lib/db_agent/viewpoint.rb
185
+ - lib/db_agent/viewpoint/base.rb
186
+ - lib/db_agent/viewpoint/delegate.rb
187
+ - lib/db_agent/viewpoint/typecheck.rb
188
+ - lib/db_agent/webapp.rb
189
+ - tasks/db.rake
190
+ - tasks/gem.rake
191
+ - tasks/test.rake
192
+ homepage: http://github.com/enspirit/dbagent
193
+ licenses:
194
+ - MIT
195
+ metadata: {}
196
+ post_install_message:
197
+ rdoc_options: []
198
+ require_paths:
199
+ - lib
200
+ required_ruby_version: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - ">="
203
+ - !ruby/object:Gem::Version
204
+ version: '0'
205
+ required_rubygems_version: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - ">="
208
+ - !ruby/object:Gem::Version
209
+ version: '0'
210
+ requirements: []
211
+ rubygems_version: 3.0.8
212
+ signing_key:
213
+ specification_version: 4
214
+ summary: A tool to migrate, spy and seed relational databases.
215
+ test_files: []