dbagile 0.0.1 → 0.0.2

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.
Files changed (55) hide show
  1. data/lib/dbagile.rb +2 -2
  2. data/lib/dbagile/adapter/sequel/data/transaction_driven.rb +1 -0
  3. data/lib/dbagile/adapter/sequel/schema/concrete_script.rb +18 -0
  4. data/lib/dbagile/adapter/sequel/schema/table_driven.rb +10 -1
  5. data/lib/dbagile/adapter/sequel/sequel_tracer.rb +1 -0
  6. data/lib/dbagile/command/bulk/commons.rb +7 -2
  7. data/lib/dbagile/command/bulk/export.rb +4 -1
  8. data/lib/dbagile/command/bulk/import.rb +32 -2
  9. data/lib/dbagile/command/schema/check.rb +2 -1
  10. data/lib/dbagile/command/schema/commons.rb +16 -1
  11. data/lib/dbagile/command/schema/dump.rb +85 -4
  12. data/lib/dbagile/command/schema/sql_script.rb +1 -0
  13. data/lib/dbagile/contract/data/dataset.rb +9 -0
  14. data/lib/dbagile/core/schema/builder.rb +8 -4
  15. data/lib/dbagile/core/schema/builder/concept_factory.rb +5 -0
  16. data/lib/dbagile/core/schema/composite.rb +1 -1
  17. data/lib/dbagile/core/schema/computations/merge.rb +1 -1
  18. data/lib/dbagile/core/schema/errors.rb +1 -3
  19. data/lib/dbagile/core/schema/logical.rb +1 -0
  20. data/lib/dbagile/core/schema/logical/attribute.rb +11 -2
  21. data/lib/dbagile/core/schema/logical/relview.rb +42 -0
  22. data/lib/dbagile/core/schema/migrate/abstract_script.rb +2 -0
  23. data/lib/dbagile/core/schema/migrate/create_view.rb +15 -0
  24. data/lib/dbagile/core/schema/migrate/drop_view.rb +15 -0
  25. data/lib/dbagile/core/schema/migrate/operation.rb +8 -6
  26. data/lib/dbagile/core/schema/migrate/stager.rb +12 -3
  27. data/lib/dbagile/core/schema/part.rb +0 -49
  28. data/lib/dbagile/core/schema/schema_object.rb +54 -0
  29. data/lib/dbagile/environment/repository.rb +2 -2
  30. data/lib/dbagile/errors.rb +3 -0
  31. data/lib/dbagile/io.rb +7 -3
  32. data/lib/dbagile/io/html.rb +48 -0
  33. data/lib/dbagile/restful.rb +1 -1
  34. data/lib/dbagile/restful/middleware/one_database.rb +1 -0
  35. data/lib/dbagile/restful/middleware/post.rb +16 -1
  36. data/lib/dbagile/tools/tuple.rb +7 -0
  37. data/test/assumptions/sequel/connect.spec +12 -9
  38. data/test/assumptions/sequel/test.db +0 -0
  39. data/test/commands.spec +4 -4
  40. data/test/commands/bulk/import.spec +26 -3
  41. data/test/commands/schema/check.spec +20 -13
  42. data/test/commands/schema/dump.spec +23 -0
  43. data/test/fixtures/basics/dbagile.idx +7 -7
  44. data/test/fixtures/basics/robust.db +0 -0
  45. data/test/fixtures/basics/test.db +0 -0
  46. data/test/fixtures/empty/dbagile.idx +1 -1
  47. data/test/restful/get/html_format.ex +12 -0
  48. data/test/run_all_suite.rb +1 -1
  49. data/test/spec_helper.rb +0 -2
  50. data/test/support/be_a_valid_json_string.rb +1 -1
  51. data/test/support/be_a_valid_yaml_string.rb +1 -1
  52. data/test/unit/core/schema/fixtures/views.yaml +5 -0
  53. data/test/unit/core/schema/part_keys.spec +12 -0
  54. data/test/unit/core/schema/yaml_load.spec +5 -0
  55. metadata +414 -244
@@ -2,7 +2,7 @@ require 'dbagile/loader'
2
2
  module DbAgile
3
3
 
4
4
  # Version of the DbAgile interface
5
- VERSION = "0.0.1".freeze
5
+ VERSION = "0.0.2".freeze
6
6
 
7
7
  # Domains recognized as valid domains inside a SQL database
8
8
  RECOGNIZED_DOMAINS = [
@@ -52,7 +52,7 @@ module DbAgile
52
52
  DbAgile::Environment.default
53
53
  end
54
54
  module_function :default_environment
55
-
55
+
56
56
  end # module DbAgile
57
57
  require 'dbagile/robustness'
58
58
  require 'dbagile/errors'
@@ -16,6 +16,7 @@ module DbAgile
16
16
  else
17
17
  db[table_name].where(proj).update(update)
18
18
  end
19
+ update
19
20
  end
20
21
 
21
22
  # @see DbAgile::Contract::Data::TransactionDriven#delete
@@ -10,6 +10,24 @@ module DbAgile
10
10
  buffer
11
11
  end
12
12
 
13
+ def create_view(conn, op, buffer)
14
+ tname = conn.send(:quote_schema_table, op.table_name)
15
+ buffer << "CREATE VIEW #{tname} AS #{op.relview.definition};" << "\n"
16
+ staged!(op)
17
+ rescue Sequel::Error => ex
18
+ buffer << "-- UNSUPPORTED: #{op.to_sql92}" << "\n"
19
+ unsupported!(op)
20
+ end
21
+
22
+ def drop_view(conn, op, buffer)
23
+ tname = conn.send(:quote_schema_table, op.table_name)
24
+ buffer << "DROP VIEW #{tname};" << "\n"
25
+ staged!(op)
26
+ rescue Sequel::Error => ex
27
+ buffer << "-- UNSUPPORTED: #{op.to_sql92}" << "\n"
28
+ unsupported!(op)
29
+ end
30
+
13
31
  def create_table(conn, op, buffer)
14
32
  gen = Sequel::Schema::Generator.new(conn)
15
33
  build_sequel_expand_generator(conn, op, gen, "")
@@ -43,7 +43,16 @@ module DbAgile
43
43
 
44
44
  # @see DbAgile::Contract::Schema::TableDriven#keys
45
45
  def keys(table_name)
46
- db.indexes(table_name).values.select{|i| i[:unique] == true}.collect{|i| i[:columns]}.sort{|a1, a2| a1.size <=> a2.size}
46
+ # take the indexes
47
+ indexes = db.indexes(table_name).values
48
+ indexes = indexes.select{|i| i[:unique] == true}.collect{|i| i[:columns]}.sort{|a1, a2| a1.size <=> a2.size}
49
+
50
+ # take single keys as well
51
+ key = db.schema(table_name).select{|pair|
52
+ pair[1][:primary_key]
53
+ }.collect{|pair| pair[0]}
54
+
55
+ key.empty? ? indexes : (indexes + [ key ])
47
56
  end
48
57
 
49
58
  end # module TableDriven
@@ -1,3 +1,4 @@
1
+ require 'logger'
1
2
  module DbAgile
2
3
  class SequelAdapter
3
4
  class SequelTracer
@@ -59,8 +59,8 @@ module DbAgile
59
59
 
60
60
  # Adds the format options for output
61
61
  def add_output_format_options(opt)
62
- opt.on('--format=X', [:csv, :text, :json, :yaml, :ruby, :xml],
63
- "Export dataset in (csv, text, json, yaml, ruby, xml)") do |value|
62
+ opt.on('--format=X', [:csv, :text, :json, :yaml, :ruby, :xml, :html],
63
+ "Export dataset in (csv, text, json, yaml, ruby, xml, html)") do |value|
64
64
  self.format = value
65
65
  end
66
66
  opt.on("--csv", "Export dataset in csv (default)"){ self.format = :csv }
@@ -68,6 +68,7 @@ module DbAgile
68
68
  opt.on("--json", "Export dataset in json"){ self.format = :json }
69
69
  opt.on("--yaml", "Export dataset in yaml"){ self.format = :yaml }
70
70
  opt.on("--xml", "Export dataset in xml"){ self.format = :xml }
71
+ opt.on("--html", "Export dataset in html"){ self.format = :html }
71
72
  opt.on("--ruby", "Export dataset as ruby code"){ self.format = :ruby }
72
73
  end
73
74
 
@@ -123,6 +124,10 @@ module DbAgile
123
124
  io_options[:text][:append_with] = '...'
124
125
  end
125
126
  end
127
+
128
+ # Adds output HTML options
129
+ def add_html_output_options(opt)
130
+ end
126
131
 
127
132
  end # module Commons
128
133
  end # module Bulk
@@ -39,6 +39,10 @@ module DbAgile
39
39
  opt.separator "\nTEXT options:"
40
40
  add_text_output_options(opt)
41
41
 
42
+ # HTML output options
43
+ opt.separator "\nHTML options:"
44
+ add_html_output_options(opt)
45
+
42
46
  # JSON output options
43
47
  opt.separator "\nJSON options:"
44
48
  add_json_output_options(opt)
@@ -85,7 +89,6 @@ module DbAgile
85
89
  # Export it
86
90
  with_io{|io|
87
91
  method = "to_#{self.format}".to_sym
88
- io = environment.output_buffer
89
92
  options = io_options[self.format]
90
93
  options[:type_system] = self.type_system if self.type_system
91
94
  ds.send(method, io, options)
@@ -23,6 +23,9 @@ module DbAgile
23
23
  # Truncate table?
24
24
  attr_accessor :truncate_table
25
25
 
26
+ # Update tuple when key already exists?
27
+ attr_accessor :update_on_existing
28
+
26
29
  # Contribute to options
27
30
  def add_options(opt)
28
31
  # Main output options
@@ -48,9 +51,13 @@ module DbAgile
48
51
  opt.on('--trace-sql', "Trace SQL statements on STDOUT (but executes them)") do |value|
49
52
  self.conn_options[:trace_sql] = true
50
53
  end
54
+ opt.on("--update", "-u", "Update tuple instead of insert when key already exists") do |value|
55
+ self.update_on_existing = true
56
+ end
51
57
  opt.on('--dry-run', "Trace SQL statements on STDOUT only, do nothing on the database") do |value|
52
58
  self.conn_options[:trace_sql] = true
53
59
  self.conn_options[:trace_only] = true
60
+ self.conn_options[:trace_buffer] = environment.output_buffer
54
61
  end
55
62
 
56
63
  opt.separator "\nRecognized format options:"
@@ -106,7 +113,7 @@ module DbAgile
106
113
  # Executes the command
107
114
  def execute_command
108
115
  with_current_connection(conn_options) do |connection|
109
-
116
+
110
117
  # Make the job now
111
118
  connection.transaction do |t|
112
119
  first = true
@@ -119,10 +126,33 @@ module DbAgile
119
126
  end
120
127
  end
121
128
 
129
+ def find_key(t, tuple)
130
+ t.keys(self.dataset).find{|key|
131
+ key.all?{|k| tuple.has_key?(k)}
132
+ }
133
+ end
134
+
122
135
  # Makes the insert job
123
136
  def make_the_job(t, tuple, first = true)
124
137
  handle_schema_modification(t, tuple) if first
125
- t.insert(self.dataset, tuple)
138
+ if self.update_on_existing
139
+
140
+ # find a candidate key
141
+ key = find_key(t, tuple)
142
+ if key.nil?
143
+ raise DbAgile::CandidateKeyNotFoundError, "Unable to find a candidate key for UPDATE"
144
+ end
145
+
146
+ # Make insert or update according to key
147
+ key_value = tuple_project(tuple, key)
148
+ if t.exists?(self.dataset, key_value)
149
+ t.update(self.dataset, tuple, key_value)
150
+ else
151
+ t.insert(self.dataset, tuple)
152
+ end
153
+ else
154
+ t.insert(self.dataset, tuple)
155
+ end
126
156
  end
127
157
 
128
158
  # Handles the schema modifications
@@ -4,7 +4,7 @@ module DbAgile
4
4
  #
5
5
  # Check a database schema
6
6
  #
7
- # Usage: dba #{command_name} [SCHEMA.yaml|announced|effective|physical]
7
+ # Usage: dba #{command_name} [options] [SCHEMA.yaml|announced|effective|physical]
8
8
  #
9
9
  # This command informs you about bad smells and good practices with relational
10
10
  # schemas (i.e. forgetting to create keys, not providing unique constraint
@@ -33,6 +33,7 @@ module DbAgile
33
33
  # Contribute to options
34
34
  def add_options(opt)
35
35
  self.check_schemas = false
36
+ add_stdin_options(opt)
36
37
  end
37
38
 
38
39
  # Executes the command
@@ -6,6 +6,9 @@ module DbAgile
6
6
  # Checks schema(s) first?
7
7
  attr_accessor :check_schemas
8
8
 
9
+ # Take schema on standard input?
10
+ attr_accessor :on_stdin
11
+
9
12
  # Schema arguments
10
13
  attr_accessor :schema_arguments
11
14
 
@@ -17,6 +20,13 @@ module DbAgile
17
20
  end
18
21
  end
19
22
 
23
+ # Adds --stdin option
24
+ def add_stdin_options(opt)
25
+ opt.on('--stdin', "Take schema on the standard command input") do
26
+ self.on_stdin = true
27
+ end
28
+ end
29
+
20
30
  # Normalizes a schema argument
21
31
  def normalize_schema_argument(argument)
22
32
  if File.exists?(argument) and File.file?(argument)
@@ -31,7 +41,7 @@ module DbAgile
31
41
  @schema_arguments = case kind_of_schema_arguments
32
42
  when :single
33
43
  if arguments.empty?
34
- [ :announced ]
44
+ on_stdin ? [ :stdin ] : [ :announced ]
35
45
  elsif arguments.size == 1
36
46
  arguments.collect{|arg| normalize_schema_argument(arg) }
37
47
  else
@@ -64,6 +74,11 @@ module DbAgile
64
74
  # Loads a given schema from a schema argument
65
75
  def load_schema(schema_argument, check = self.check_schemas)
66
76
  schema = case schema_argument
77
+ when :stdin
78
+ r = environment.input_buffer.read
79
+ s = DbAgile::Core::Schema::yaml_load(r)
80
+ s.schema_identifier = "--stdin"
81
+ s
67
82
  when Symbol
68
83
  with_current_database do |database|
69
84
  case schema_argument
@@ -4,13 +4,32 @@ module DbAgile
4
4
  #
5
5
  # Dump a schema of the current database
6
6
  #
7
- # Usage: dba #{command_name} [announced|effective|physical]
7
+ # Usage: dba #{command_name} [options] [announced|effective|physical]
8
8
  #
9
9
  # This command dumps the schema of the current database on the output console.
10
10
  # Without argument, the announced schema is implicit. This command uses the
11
11
  # fallback chain (announced -> effective -> physical) and has no side-effect
12
12
  # on the database itself (read-only).
13
13
  #
14
+ # Schema filtering is possible via the --include and --exclude options, which
15
+ # allow including/excluding specific schema object kinds. When used conjointly,
16
+ # the semantics is '--include AND NOT(--exclude)'. The different object kinds
17
+ # are:
18
+ # * logical (all logical objects, see indented list below)
19
+ # * table (base tables)
20
+ # * view (derived tables, aka views)
21
+ # * constraint (all constraints, see indented list below)
22
+ # * candidate_key (all candidate keys, primary or not)
23
+ # * primary_key (all primary keys)
24
+ # * foreign_key (all foreign keys)
25
+ # * physical (all physical objects, see indented list below)
26
+ # * index (all physical indexes)
27
+ #
28
+ # A typical usage of schema filtering is to generate a script that drops all
29
+ # constraints at once by chaining this command with sql-script:
30
+ #
31
+ # dba schema:dump --include=constraint | dba schema:sql-script --stdin drop
32
+ #
14
33
  # NOTE: Schema-checking is on by default, which may lead to checking errors,
15
34
  # typically when reverse engineering poorly designed databases. Doing so
16
35
  # immediately informs you about a potential problem.
@@ -21,22 +40,84 @@ module DbAgile
21
40
  include Schema::Commons
22
41
  Command::build_me(self, __FILE__)
23
42
 
43
+ QUERIES = {
44
+ :logical => [:logical?],
45
+ :table => [:attribute?, :constraint?],
46
+ :view => [:relview?],
47
+ :constraint => [:constraint?],
48
+ :candidate_key => [:candidate_key],
49
+ :primary_key => [:primary_key],
50
+ :foreign_key => [:foreign_key],
51
+ :physical => [:physical?],
52
+ :index => [:index?]
53
+ }
54
+
55
+ # Returns :single
56
+ def kind_of_schema_arguments
57
+ :single
58
+ end
59
+
60
+ # Include and exclude options
61
+ attr_accessor :include_kind, :exclude_kind
62
+
63
+ # Marks a kind a begin included
64
+ def include_kind!(kind)
65
+ if queries = QUERIES[kind]
66
+ self.include_kind ||= []
67
+ self.include_kind += queries
68
+ else
69
+ raise DbAgile::Error, "Unrecognized object kind #{kind}"
70
+ end
71
+ end
72
+
73
+ # Marks a kind a begin excluded
74
+ def exclude_kind!(kind)
75
+ if queries = QUERIES[kind]
76
+ self.exclude_kind ||= []
77
+ self.exclude_kind += queries
78
+ else
79
+ raise DbAgile::Error, "Unrecognized object kind #{kind}"
80
+ end
81
+ end
82
+
24
83
  # Contribute to options
25
84
  def add_options(opt)
26
85
  opt.separator nil
27
86
  opt.separator "Options:"
28
87
  add_check_options(opt)
88
+ add_stdin_options(opt)
89
+ opt.on('--include=x,y,z', Array,
90
+ "Include object kinds (logical, physical, candidate_key, ...)") do |values|
91
+ values.each{|value| include_kind!(value.to_sym)}
92
+ end
93
+ opt.on('--exclude=x,y,z', Array,
94
+ "Exclude object kinds (logical, physical, candidate_key, ...)") do |values|
95
+ values.each{|value| exclude_kind!(value.to_sym)}
96
+ end
29
97
  end
30
98
 
31
- # Returns :single
32
- def kind_of_schema_arguments
33
- :single
99
+ # Returns the kind of an object
100
+ def include?(obj)
101
+ if include_kind && !include_kind.any?{|kind| obj.send(kind)}
102
+ return false
103
+ end
104
+ if exclude_kind && exclude_kind.any?{|kind| obj.send(kind)}
105
+ return false
106
+ end
107
+ true
108
+ end
109
+
110
+ # Filters the schema according to --select and --reject
111
+ # options.
112
+ def filter_shema(schema)
113
+ schema.filter{|obj| include?(obj)}
34
114
  end
35
115
 
36
116
  # Executes the command
37
117
  def execute_command
38
118
  with_schema do |schema|
39
119
  say("# Schema of #{schema.schema_identifier.inspect}", :magenta)
120
+ schema = filter_shema(schema)
40
121
  flush(schema.to_yaml)
41
122
  schema
42
123
  end
@@ -49,6 +49,7 @@ module DbAgile
49
49
  opt.separator nil
50
50
  opt.separator "Options:"
51
51
  add_check_options(opt)
52
+ add_stdin_options(opt)
52
53
  end
53
54
 
54
55
  # Returns :single
@@ -45,6 +45,15 @@ module DbAgile
45
45
  DbAgile::IO::XML::to_xml(self, self.columns, buffer, options)
46
46
  end
47
47
 
48
+ #
49
+ # Outputs this dataset as a HTML string
50
+ #
51
+ # @return [...] the buffer itself
52
+ #
53
+ def to_html(buffer = "", options = {})
54
+ DbAgile::IO::HTML::to_html(self, self.columns, buffer, options)
55
+ end
56
+
48
57
  #
49
58
  # Outputs this dataset as a Ruby Array of hashes string
50
59
  #
@@ -117,10 +117,14 @@ module DbAgile
117
117
 
118
118
  # Starts a relvar section
119
119
  def relvar(name, hash = nil, &block)
120
- block = lambda{ _natural(hash) } unless block
121
- name = coerce_relvar_name(name)
122
- relvar = (_peek(:logical)[name] ||= build_relvar(name))
123
- _push(:relvar, relvar, &block)
120
+ if String === hash
121
+ (_peek(:logical)[name] ||= build_relview(name, hash))
122
+ else
123
+ block = lambda{ _natural(hash) } unless block
124
+ name = coerce_relvar_name(name)
125
+ relvar = (_peek(:logical)[name] ||= build_relvar(name))
126
+ _push(:relvar, relvar, &block)
127
+ end
124
128
  rescue SByC::TypeSystem::CoercionError => ex
125
129
  invalid!("Invalid relvar definition (#{name}): #{ex.message}")
126
130
  end
@@ -18,6 +18,11 @@ module DbAgile
18
18
  def build_relvar(name)
19
19
  Schema::Logical::Relvar.new(name)
20
20
  end
21
+
22
+ # Builds a relvar
23
+ def build_relview(name, defn)
24
+ Schema::Logical::Relview.new(name, defn)
25
+ end
21
26
 
22
27
  # Builds a heading
23
28
  def build_heading
@@ -140,7 +140,7 @@ module DbAgile
140
140
  end
141
141
 
142
142
  # @see DbAgile::Core::SchemaObject
143
- def part_keys(sort = false)
143
+ def part_keys(sort = (RUBY_VERSION < "1.9"))
144
144
  if sort
145
145
  @composite_parts.keys.sort{|k1,k2| k1.to_s <=> k2.to_s}
146
146
  else
@@ -14,7 +14,7 @@ module DbAgile
14
14
  end
15
15
 
16
16
  # Computes key differences
17
- left_keys, right_keys = left.part_keys(true), right.part_keys(true)
17
+ left_keys, right_keys = left.part_keys, right.part_keys
18
18
  left_only = left_keys - right_keys
19
19
  right_only = right_keys - left_keys
20
20
  commons = left_keys & right_keys