dbagent 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []