coupler 0.0.1-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (258) hide show
  1. data/.document +5 -0
  2. data/.gitmodules +3 -0
  3. data/.rvmrc +1 -0
  4. data/.vimrc +40 -0
  5. data/Gemfile +27 -0
  6. data/Gemfile.lock +71 -0
  7. data/LICENSE +20 -0
  8. data/NOTES +6 -0
  9. data/README.rdoc +18 -0
  10. data/Rakefile +42 -0
  11. data/TODO +11 -0
  12. data/VERSION +1 -0
  13. data/bin/coupler +7 -0
  14. data/db/.gitignore +6 -0
  15. data/db/migrate/001_initial_schema.rb +166 -0
  16. data/db/migrate/002_stub.rb +4 -0
  17. data/db/migrate/003_stub.rb +4 -0
  18. data/db/migrate/004_create_comparisons.rb +28 -0
  19. data/db/migrate/005_move_database_name.rb +19 -0
  20. data/db/migrate/006_upgrade_comparisons.rb +34 -0
  21. data/db/migrate/007_add_which_to_comparisons.rb +23 -0
  22. data/db/migrate/008_add_result_field_to_transformations.rb +33 -0
  23. data/db/migrate/009_add_generated_flag_to_fields.rb +13 -0
  24. data/db/migrate/010_create_imports.rb +24 -0
  25. data/db/migrate/011_add_primary_key_type.rb +13 -0
  26. data/db/migrate/012_add_transformed_with_to_resources.rb +13 -0
  27. data/db/migrate/013_add_run_count_to_scenarios.rb +13 -0
  28. data/db/migrate/014_add_last_accessed_at_to_some_tables.rb +13 -0
  29. data/db/migrate/015_add_run_number_to_results.rb +15 -0
  30. data/db/migrate/016_fix_scenario_run_count.rb +27 -0
  31. data/db/migrate/017_rename_comparison_columns.rb +14 -0
  32. data/db/migrate/018_fix_scenario_linkage_type.rb +8 -0
  33. data/db/migrate/019_add_columns_to_imports.rb +24 -0
  34. data/db/migrate/020_rename_import_columns.rb +12 -0
  35. data/db/migrate/021_add_fields_to_connections.rb +15 -0
  36. data/db/migrate/022_remove_database_name_from_resources.rb +11 -0
  37. data/features/connections.feature +28 -0
  38. data/features/matchers.feature +35 -0
  39. data/features/projects.feature +11 -0
  40. data/features/resources.feature +62 -0
  41. data/features/scenarios.feature +45 -0
  42. data/features/step_definitions/coupler_steps.rb +145 -0
  43. data/features/step_definitions/matchers_steps.rb +26 -0
  44. data/features/step_definitions/resources_steps.rb +12 -0
  45. data/features/step_definitions/scenarios_steps.rb +7 -0
  46. data/features/step_definitions/transformations_steps.rb +3 -0
  47. data/features/support/env.rb +128 -0
  48. data/features/transformations.feature +22 -0
  49. data/features/wizard.feature +10 -0
  50. data/gfx/coupler-header.svg +213 -0
  51. data/gfx/coupler-sidebar.svg +656 -0
  52. data/gfx/coupler.svg +184 -0
  53. data/gfx/icon.svg +75 -0
  54. data/lib/coupler/base.rb +63 -0
  55. data/lib/coupler/config.rb +128 -0
  56. data/lib/coupler/data_uploader.rb +20 -0
  57. data/lib/coupler/database.rb +31 -0
  58. data/lib/coupler/extensions/connections.rb +57 -0
  59. data/lib/coupler/extensions/exceptions.rb +58 -0
  60. data/lib/coupler/extensions/imports.rb +43 -0
  61. data/lib/coupler/extensions/jobs.rb +21 -0
  62. data/lib/coupler/extensions/matchers.rb +64 -0
  63. data/lib/coupler/extensions/projects.rb +62 -0
  64. data/lib/coupler/extensions/resources.rb +89 -0
  65. data/lib/coupler/extensions/results.rb +100 -0
  66. data/lib/coupler/extensions/scenarios.rb +50 -0
  67. data/lib/coupler/extensions/transformations.rb +70 -0
  68. data/lib/coupler/extensions/transformers.rb +58 -0
  69. data/lib/coupler/extensions.rb +16 -0
  70. data/lib/coupler/helpers.rb +121 -0
  71. data/lib/coupler/import_buffer.rb +48 -0
  72. data/lib/coupler/logger.rb +16 -0
  73. data/lib/coupler/models/common_model.rb +104 -0
  74. data/lib/coupler/models/comparison.rb +166 -0
  75. data/lib/coupler/models/connection.rb +59 -0
  76. data/lib/coupler/models/field.rb +55 -0
  77. data/lib/coupler/models/import.rb +238 -0
  78. data/lib/coupler/models/job.rb +42 -0
  79. data/lib/coupler/models/jobify.rb +17 -0
  80. data/lib/coupler/models/matcher.rb +36 -0
  81. data/lib/coupler/models/project.rb +40 -0
  82. data/lib/coupler/models/resource.rb +287 -0
  83. data/lib/coupler/models/result.rb +92 -0
  84. data/lib/coupler/models/scenario/runner.rb +357 -0
  85. data/lib/coupler/models/scenario.rb +115 -0
  86. data/lib/coupler/models/transformation.rb +117 -0
  87. data/lib/coupler/models/transformer/runner.rb +28 -0
  88. data/lib/coupler/models/transformer.rb +110 -0
  89. data/lib/coupler/models.rb +30 -0
  90. data/lib/coupler/runner.rb +76 -0
  91. data/lib/coupler/scheduler.rb +56 -0
  92. data/lib/coupler.rb +34 -0
  93. data/log/.gitignore +1 -0
  94. data/misc/README +5 -0
  95. data/misc/jruby-json.license +57 -0
  96. data/misc/rack-flash.license +22 -0
  97. data/script/dbconsole.rb +5 -0
  98. data/src/edu/vanderbilt/coupler/Main.java +116 -0
  99. data/src/edu/vanderbilt/coupler/jruby.properties +1 -0
  100. data/tasks/annotations.rake +84 -0
  101. data/tasks/db.rake +120 -0
  102. data/tasks/environment.rake +12 -0
  103. data/tasks/jeweler.rake +43 -0
  104. data/tasks/package.rake +58 -0
  105. data/tasks/rdoc.rake +13 -0
  106. data/tasks/test.rake +63 -0
  107. data/tasks/vendor.rake +43 -0
  108. data/test/README.txt +6 -0
  109. data/test/config.yml +9 -0
  110. data/test/coupler/models/test_import.rb +221 -0
  111. data/test/factories.rb +91 -0
  112. data/test/fixtures/duplicate-keys.csv +5 -0
  113. data/test/fixtures/no-headers.csv +50 -0
  114. data/test/fixtures/people.csv +51 -0
  115. data/test/fixtures/varying-row-size.csv +4 -0
  116. data/test/helper.rb +156 -0
  117. data/test/integration/extensions/test_connections.rb +80 -0
  118. data/test/integration/extensions/test_imports.rb +94 -0
  119. data/test/integration/extensions/test_jobs.rb +52 -0
  120. data/test/integration/extensions/test_matchers.rb +134 -0
  121. data/test/integration/extensions/test_projects.rb +82 -0
  122. data/test/integration/extensions/test_resources.rb +150 -0
  123. data/test/integration/extensions/test_results.rb +89 -0
  124. data/test/integration/extensions/test_scenarios.rb +88 -0
  125. data/test/integration/extensions/test_transformations.rb +113 -0
  126. data/test/integration/extensions/test_transformers.rb +80 -0
  127. data/test/integration/test_field.rb +45 -0
  128. data/test/integration/test_import.rb +78 -0
  129. data/test/integration/test_running_scenarios.rb +379 -0
  130. data/test/integration/test_transformation.rb +56 -0
  131. data/test/integration/test_transforming.rb +154 -0
  132. data/test/table_sets.rb +76 -0
  133. data/test/unit/models/test_common_model.rb +130 -0
  134. data/test/unit/models/test_comparison.rb +619 -0
  135. data/test/unit/models/test_connection.rb +115 -0
  136. data/test/unit/models/test_field.rb +99 -0
  137. data/test/unit/models/test_import.rb +130 -0
  138. data/test/unit/models/test_job.rb +115 -0
  139. data/test/unit/models/test_matcher.rb +82 -0
  140. data/test/unit/models/test_project.rb +102 -0
  141. data/test/unit/models/test_resource.rb +564 -0
  142. data/test/unit/models/test_result.rb +90 -0
  143. data/test/unit/models/test_scenario.rb +199 -0
  144. data/test/unit/models/test_transformation.rb +193 -0
  145. data/test/unit/models/test_transformer.rb +188 -0
  146. data/test/unit/test_base.rb +60 -0
  147. data/test/unit/test_data_uploader.rb +27 -0
  148. data/test/unit/test_database.rb +23 -0
  149. data/test/unit/test_helpers.rb +58 -0
  150. data/test/unit/test_logger.rb +10 -0
  151. data/test/unit/test_models.rb +12 -0
  152. data/test/unit/test_runner.rb +76 -0
  153. data/test/unit/test_scheduler.rb +66 -0
  154. data/uploads/.gitignore +2 -0
  155. data/vendor/java/.gitignore +5 -0
  156. data/webroot/public/css/960.css +1 -0
  157. data/webroot/public/css/dataTables.css +1057 -0
  158. data/webroot/public/css/jquery-ui.css +572 -0
  159. data/webroot/public/css/jquery.treeview.css +68 -0
  160. data/webroot/public/css/reset.css +1 -0
  161. data/webroot/public/css/style.css +504 -0
  162. data/webroot/public/css/text.css +1 -0
  163. data/webroot/public/favicon.ico +0 -0
  164. data/webroot/public/images/12_col.gif +0 -0
  165. data/webroot/public/images/16_col.gif +0 -0
  166. data/webroot/public/images/add.png +0 -0
  167. data/webroot/public/images/ajax-loader.gif +0 -0
  168. data/webroot/public/images/cog.png +0 -0
  169. data/webroot/public/images/coupler.png +0 -0
  170. data/webroot/public/images/foo.png +0 -0
  171. data/webroot/public/images/hammer.png +0 -0
  172. data/webroot/public/images/header.png +0 -0
  173. data/webroot/public/images/home.gif +0 -0
  174. data/webroot/public/images/jobs.gif +0 -0
  175. data/webroot/public/images/sidebar-bottom.png +0 -0
  176. data/webroot/public/images/sidebar.png +0 -0
  177. data/webroot/public/images/treeview-default-line.gif +0 -0
  178. data/webroot/public/images/treeview-default.gif +0 -0
  179. data/webroot/public/images/ui-anim_basic_16x16.gif +0 -0
  180. data/webroot/public/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  181. data/webroot/public/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  182. data/webroot/public/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  183. data/webroot/public/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  184. data/webroot/public/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  185. data/webroot/public/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  186. data/webroot/public/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  187. data/webroot/public/images/ui-bg_highlight-hard_30_565356_1x100.png +0 -0
  188. data/webroot/public/images/ui-bg_highlight-hard_75_888588_1x100.png +0 -0
  189. data/webroot/public/images/ui-bg_highlight-soft_30_6e3b3a_1x100.png +0 -0
  190. data/webroot/public/images/ui-bg_highlight-soft_35_8e8b8e_1x100.png +0 -0
  191. data/webroot/public/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  192. data/webroot/public/images/ui-icons_222222_256x240.png +0 -0
  193. data/webroot/public/images/ui-icons_2e83ff_256x240.png +0 -0
  194. data/webroot/public/images/ui-icons_454545_256x240.png +0 -0
  195. data/webroot/public/images/ui-icons_888888_256x240.png +0 -0
  196. data/webroot/public/images/ui-icons_cd0a0a_256x240.png +0 -0
  197. data/webroot/public/images/ui-icons_ffffff_256x240.png +0 -0
  198. data/webroot/public/js/ajaxupload.js +673 -0
  199. data/webroot/public/js/application.js +40 -0
  200. data/webroot/public/js/jquery-ui.combobox.js +98 -0
  201. data/webroot/public/js/jquery-ui.js +9867 -0
  202. data/webroot/public/js/jquery-ui.min.js +559 -0
  203. data/webroot/public/js/jquery.dataTables.min.js +587 -0
  204. data/webroot/public/js/jquery.min.js +154 -0
  205. data/webroot/public/js/jquery.timeago.js +140 -0
  206. data/webroot/public/js/jquery.tooltip.min.js +19 -0
  207. data/webroot/public/js/jquery.treeview.min.js +15 -0
  208. data/webroot/public/js/resource.js +11 -0
  209. data/webroot/public/js/results.js +56 -0
  210. data/webroot/public/js/transformations.js +95 -0
  211. data/webroot/views/connections/index.erb +5 -0
  212. data/webroot/views/connections/list.erb +34 -0
  213. data/webroot/views/connections/new.erb +55 -0
  214. data/webroot/views/connections/show.erb +36 -0
  215. data/webroot/views/imports/edit.erb +60 -0
  216. data/webroot/views/imports/form.erb +81 -0
  217. data/webroot/views/imports/new.erb +89 -0
  218. data/webroot/views/index.erb +12 -0
  219. data/webroot/views/jobs/index.erb +7 -0
  220. data/webroot/views/jobs/list.erb +24 -0
  221. data/webroot/views/layout.erb +38 -0
  222. data/webroot/views/matchers/form.erb +250 -0
  223. data/webroot/views/matchers/list.erb +32 -0
  224. data/webroot/views/projects/form.erb +14 -0
  225. data/webroot/views/projects/index.erb +96 -0
  226. data/webroot/views/projects/show.erb +24 -0
  227. data/webroot/views/resources/edit.erb +88 -0
  228. data/webroot/views/resources/index.erb +5 -0
  229. data/webroot/views/resources/list.erb +27 -0
  230. data/webroot/views/resources/new.erb +121 -0
  231. data/webroot/views/resources/show.erb +86 -0
  232. data/webroot/views/resources/transform.erb +2 -0
  233. data/webroot/views/results/csv.erb +12 -0
  234. data/webroot/views/results/details.erb +15 -0
  235. data/webroot/views/results/index.erb +2 -0
  236. data/webroot/views/results/list.erb +22 -0
  237. data/webroot/views/results/record.erb +24 -0
  238. data/webroot/views/results/show.erb +68 -0
  239. data/webroot/views/scenarios/index.erb +5 -0
  240. data/webroot/views/scenarios/list.erb +20 -0
  241. data/webroot/views/scenarios/new.erb +99 -0
  242. data/webroot/views/scenarios/run.erb +2 -0
  243. data/webroot/views/scenarios/show.erb +50 -0
  244. data/webroot/views/sidebar.erb +106 -0
  245. data/webroot/views/transformations/create.erb +115 -0
  246. data/webroot/views/transformations/for.erb +16 -0
  247. data/webroot/views/transformations/index.erb +2 -0
  248. data/webroot/views/transformations/list.erb +29 -0
  249. data/webroot/views/transformations/new.erb +126 -0
  250. data/webroot/views/transformations/preview.erb +46 -0
  251. data/webroot/views/transformers/edit.erb +6 -0
  252. data/webroot/views/transformers/form.erb +58 -0
  253. data/webroot/views/transformers/index.erb +2 -0
  254. data/webroot/views/transformers/list.erb +25 -0
  255. data/webroot/views/transformers/new.erb +5 -0
  256. data/webroot/views/transformers/preview.erb +23 -0
  257. data/webroot/views/transformers/show.erb +0 -0
  258. metadata +558 -0
@@ -0,0 +1,48 @@
1
+ module Coupler
2
+ # This class is used during resource transformation. Its purpose
3
+ # is for mass inserts into the local database for speed.
4
+ class ImportBuffer
5
+ attr_writer :dataset
6
+ def initialize(columns, dataset, &progress)
7
+ @columns = columns
8
+ @dataset = dataset
9
+ @mutex = Mutex.new
10
+ @progress = progress
11
+ @pending = 0
12
+ @max_query_size = 1_048_576
13
+ end
14
+
15
+ def add(row)
16
+ fragment = " " + @dataset.literal(row.is_a?(Hash) ? row.values_at(*@columns) : row) + ","
17
+ @mutex.synchronize do
18
+ init_query if @query.nil?
19
+ if (@query.length + fragment.length) > @max_query_size
20
+ flush(false)
21
+ init_query
22
+ end
23
+ @query << fragment
24
+ @pending += 1
25
+ end
26
+ end
27
+
28
+ def flush(lock = true)
29
+ begin
30
+ @mutex.lock if lock
31
+ if @query
32
+ @dataset.db.run(@query.chomp(","))
33
+ @progress.call(@pending) if @progress
34
+ @pending = 0
35
+ @query = nil
36
+ end
37
+ ensure
38
+ @mutex.unlock if lock
39
+ end
40
+ end
41
+
42
+ private
43
+ def init_query
44
+ @query = String.alloc(@max_query_size)
45
+ @query << @dataset.insert_sql(@columns, Sequel::LiteralString.new('VALUES'))
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,16 @@
1
+ module Coupler
2
+ class Logger < Delegator
3
+ include Singleton
4
+
5
+ def initialize
6
+ log_path = Base.settings.log_path
7
+ Dir.mkdir(log_path) if !File.exist?(log_path)
8
+ @logger = ::Logger.new(File.join(log_path, "#{Base.settings.environment}.log"))
9
+ super(@logger)
10
+ end
11
+
12
+ def __getobj__
13
+ @logger
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,104 @@
1
+ module Coupler
2
+ module Models
3
+ module CommonModel
4
+ module ClassMethods
5
+ def create!(*args)
6
+ new(*args).save!
7
+ end
8
+
9
+ def recently_accessed
10
+ col = columns.include?(:last_accessed_at) ? :last_accessed_at : :updated_at
11
+ order(col.desc).limit(3).all
12
+ end
13
+
14
+ def as_of_version(id, version)
15
+ versions_dataset[:current_id => id, :version => version]
16
+ end
17
+
18
+ def as_of_time(id, time)
19
+ versions_dataset.filter(["current_id = ? AND updated_at <= ?", id, time]).first
20
+ end
21
+
22
+ def versions_table_name
23
+ "#{table_name}_versions".to_sym
24
+ end
25
+
26
+ def versions_dataset
27
+ db[versions_table_name]
28
+ end
29
+
30
+ def const_missing(name)
31
+ Models.const_missing(name)
32
+ end
33
+ end
34
+
35
+ @@versioned = {}
36
+ def self.included(base)
37
+ base.extend(ClassMethods)
38
+ base.raise_on_save_failure = false
39
+ base.plugin :validation_helpers
40
+
41
+ # decide whether or not to version this model
42
+ versions_table_name = base.versions_table_name
43
+ if base.db.tables.include?(versions_table_name)
44
+ @@versioned[base] = versions_table_name
45
+ base.send(:attr_accessor, :delete_versions_on_destroy)
46
+ end
47
+ end
48
+
49
+ def before_create
50
+ super
51
+ now = Time.now
52
+ self[:created_at] = now
53
+ self[:updated_at] = now
54
+ end
55
+
56
+ def before_update
57
+ super
58
+ now = Time.now
59
+ self[:updated_at] = now
60
+ end
61
+
62
+ def before_save
63
+ super
64
+ if @@versioned[self.class] && !@skip_new_version
65
+ self[:version] = self[:version].nil? ? 1 : self[:version] + 1
66
+ end
67
+ end
68
+
69
+ def after_save
70
+ super
71
+ if @skip_new_version
72
+ @skip_new_version = nil
73
+ else
74
+ if versions_table_name = @@versioned[self.class]
75
+ dataset = self.db[versions_table_name]
76
+ hash = self.values.clone
77
+ hash[:current_id] = hash.delete(:id)
78
+ dataset.insert(hash)
79
+ end
80
+ end
81
+ end
82
+
83
+ def after_destroy
84
+ super
85
+ if @delete_versions_on_destroy && (versions_table_name = @@versioned[self.class])
86
+ dataset = self.db[versions_table_name]
87
+ dataset.filter(:current_id => id).delete
88
+ end
89
+ end
90
+
91
+ def save!(*args)
92
+ if !save(*args)
93
+ raise "couldn't save: " + errors.full_messages.join("; ")
94
+ end
95
+ self
96
+ end
97
+
98
+ def touch!
99
+ @skip_new_version = true
100
+ update(:last_accessed_at => Time.now)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,166 @@
1
+ module Coupler
2
+ module Models
3
+ class Comparison < Sequel::Model
4
+ include CommonModel
5
+
6
+ OPERATORS = {
7
+ "equals" => "=",
8
+ "does_not_equal" => "!=",
9
+ "greater_than" => ">",
10
+ "less_than" => "<",
11
+ }
12
+ TYPES = %w{field integer string}
13
+
14
+ many_to_one :matcher
15
+ plugin :serialization, :marshal, :raw_lhs_value, :raw_rhs_value
16
+
17
+ def lhs_rhs_value(name)
18
+ case self[:"#{name}_type"]
19
+ when "field"
20
+ Field[:id => send("raw_#{name}_value")]
21
+ else
22
+ send("raw_#{name}_value")
23
+ end
24
+ end
25
+ def lhs_value; lhs_rhs_value("lhs"); end
26
+ def rhs_value; lhs_rhs_value("rhs"); end
27
+
28
+ def lhs_rhs_label(name)
29
+ case self[:"#{name}_type"]
30
+ when "field"
31
+ field = lhs_rhs_value(name)
32
+ result = field.name
33
+ resource_name = field.resource.name
34
+ if self[:"#{name}_which"]
35
+ resource_name += %{<span class="sup">#{self[:"#{name}_which"]}</span>}
36
+ end
37
+ result += " (#{resource_name})"
38
+ else
39
+ lhs_rhs_value(name).inspect
40
+ end
41
+ end
42
+ def lhs_label; lhs_rhs_label("lhs"); end
43
+ def rhs_label; lhs_rhs_label("rhs"); end
44
+
45
+ def fields
46
+ result = []
47
+ result << lhs_value if lhs_type == 'field'
48
+ result << rhs_value if rhs_type == 'field'
49
+ result
50
+ end
51
+
52
+ def operator_symbol
53
+ OPERATORS[operator]
54
+ end
55
+
56
+ def apply(dataset, which = nil)
57
+ lhs = lhs_type == 'field' ? lhs_value.name.to_sym : lhs_value
58
+ rhs = rhs_type == 'field' ? rhs_value.name.to_sym : rhs_value
59
+ if !blocking?
60
+ filters = []
61
+ tmp = dataset.opts
62
+ opts = {
63
+ :select => tmp[:select] ? tmp[:select].dup : [],
64
+ :order => tmp[:order] ? tmp[:order].dup : []
65
+ }
66
+
67
+ fields =
68
+ case which
69
+ when nil then lhs == rhs ? [lhs] : [lhs, rhs]
70
+ when 0 then [lhs]
71
+ when 1 then [rhs]
72
+ end
73
+ fields.each_with_index do |field, i|
74
+ index = i == 0 ? 0 : -1
75
+
76
+ # NOTE: This assumes that the presence of a field name in the
77
+ # select array implies that the filters for it are already in
78
+ # place. I don't want to go searching through Sequel's filter
79
+ # expressions to find out what's in there.
80
+ if !opts[:select].include?(field)
81
+ opts[:select].push(field)
82
+ opts[:order].push(field)
83
+ opts[:modified] = true
84
+ filters.push(~{field => nil})
85
+ end
86
+ end
87
+ if opts.delete(:modified)
88
+ dataset = dataset.clone(opts).filter(*filters)
89
+ end
90
+ else
91
+ # Figure out which side to apply this comparison to.
92
+ tmp_which = nil
93
+ if !which.nil?
94
+ if lhs_type == 'field' && rhs_type == 'field'
95
+ if lhs_which == rhs_which
96
+ tmp_which = lhs_which == 1 ? 0 : 1
97
+ else
98
+ raise "unsupported" # FIXME
99
+ end
100
+ elsif lhs_type == 'field'
101
+ tmp_which = lhs_which == 1 ? 0 : 1
102
+ elsif rhs_type == 'field'
103
+ tmp_which = rhs_which == 1 ? 0 : 1
104
+ else
105
+ # Doesn't matter. Apply to either side.
106
+ end
107
+ end
108
+
109
+ if which.nil? || tmp_which.nil? || which == tmp_which
110
+ expr = Sequel::SQL::BooleanExpression.new(operator_symbol.to_sym, lhs, rhs)
111
+ dataset = dataset.filter(expr)
112
+ end
113
+ end
114
+ dataset
115
+ end
116
+
117
+ def blocking?
118
+ lhs_type != 'field' || rhs_type != 'field' || lhs_which == rhs_which || operator != 'equals'
119
+ end
120
+
121
+ def cross_match?
122
+ lhs_type == 'field' && rhs_type == 'field' && lhs_which != rhs_which && lhs_value.id != rhs_value.id && lhs_value.resource_id == rhs_value.resource_id
123
+ end
124
+
125
+ private
126
+ def coerce_value(type, value)
127
+ case type
128
+ when "field", "integer"
129
+ value.to_i
130
+ else
131
+ value
132
+ end
133
+ end
134
+
135
+ def before_validation
136
+ super
137
+ self.lhs_which ||= 1 if lhs_type == 'field'
138
+ self.rhs_which ||= 2 if rhs_type == 'field'
139
+ end
140
+
141
+ def validate
142
+ super
143
+ validates_presence [:raw_lhs_value, :raw_rhs_value]
144
+ validates_includes TYPES, [:lhs_type, :rhs_type]
145
+ validates_includes OPERATORS.keys, :operator
146
+ validates_includes [1, 2], :lhs_which if lhs_type == 'field'
147
+ validates_includes [1, 2], :rhs_which if rhs_type == 'field'
148
+
149
+ if lhs_type == 'field' && rhs_type == 'field' && (lhs_field = lhs_value) && (rhs_field = rhs_value)
150
+ if lhs_field[:type] != rhs_field[:type]
151
+ errors.add(:base, "Comparing fields of different types is currently disallowed.")
152
+ end
153
+ if lhs_which != rhs_which && operator != 'equals'
154
+ errors.add(:operator, "is invalid; can't compare fields with anything but equals at the moment.")
155
+ end
156
+ end
157
+ end
158
+
159
+ def before_save
160
+ self.raw_lhs_value = coerce_value(lhs_type, raw_lhs_value)
161
+ self.raw_rhs_value = coerce_value(rhs_type, raw_rhs_value)
162
+ super
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,59 @@
1
+ module Coupler
2
+ module Models
3
+ class Connection < Sequel::Model
4
+ include CommonModel
5
+
6
+ ADAPTERS = [%w{mysql MySQL h2 H2}]
7
+
8
+ one_to_many :resources
9
+
10
+ def database(&block)
11
+ Sequel.connect(connection_string, {
12
+ :loggers => [Coupler::Logger.instance],
13
+ :max_connections => 20
14
+ }, &block)
15
+ end
16
+
17
+ def deletable?
18
+ resources_dataset.count == 0
19
+ end
20
+
21
+ private
22
+ def connection_string
23
+ case adapter
24
+ when 'mysql'
25
+ misc = '&zeroDateTimeBehavior=convertToNull'
26
+ "jdbc:mysql://%s:%d/%s?user=%s&password=%s%s" % [
27
+ host, port, database_name, username, password, misc
28
+ ]
29
+ when 'h2'
30
+ "jdbc:h2:#{path}"
31
+ end
32
+ end
33
+
34
+ def before_validation
35
+ super
36
+ self.slug ||= name.downcase.gsub(/\s+/, "_") if name
37
+ end
38
+
39
+ def validate
40
+ super
41
+ validates_presence :name
42
+ validates_unique :name, :slug
43
+
44
+ begin
45
+ database { |db| db.test_connection }
46
+ rescue Sequel::DatabaseConnectionError, Sequel::DatabaseError => e
47
+ errors.add(:base, "Couldn't connect to the database")
48
+ end
49
+ end
50
+
51
+ def before_destroy
52
+ super
53
+
54
+ # Prevent destruction of connections in use by resources.
55
+ deletable?
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,55 @@
1
+ module Coupler
2
+ module Models
3
+ class Field < Sequel::Model
4
+ include CommonModel
5
+ many_to_one :resource
6
+ one_to_many :transformations, :key => :source_field_id
7
+
8
+ def original_column_options
9
+ { :name => name, :type => db_type, :primary_key => is_primary_key }
10
+ end
11
+
12
+ def local_column_options
13
+ { :name => name, :type => final_db_type,
14
+ :primary_key => is_primary_key }
15
+ end
16
+
17
+ def final_type
18
+ local_type || self[:type]
19
+ end
20
+
21
+ def final_db_type
22
+ local_db_type || db_type
23
+ end
24
+
25
+ def scenarios_dataset
26
+ marshalled_id = [Marshal.dump(id)].pack('m')
27
+ Scenario.
28
+ select(:scenarios.*).
29
+ filter({:project_id => resource.project_id} & ({:resource_1_id => resource_id} | {:resource_2_id => resource_id})).
30
+ join(Matcher, :scenario_id => :id).
31
+ join(Comparison, :matcher_id => :id).
32
+ filter({:lhs_type => 'field', :raw_lhs_value => marshalled_id} | {:rhs_type => 'field', :raw_rhs_value => marshalled_id})
33
+ end
34
+
35
+ def name_sym
36
+ @name_sym ||= name.to_sym
37
+ end
38
+
39
+ private
40
+ def validate
41
+ super
42
+ validates_presence [:name, :resource_id]
43
+ validates_unique [:name, :resource_id]
44
+ end
45
+
46
+ def before_save
47
+ super
48
+ case is_primary_key
49
+ when TrueClass, 1
50
+ self.is_selected = 1
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,238 @@
1
+ module Coupler
2
+ module Models
3
+ class Import < Sequel::Model
4
+ include CommonModel
5
+
6
+ # NOTE: yoinked from FasterCSV
7
+ # A Regexp used to find and convert some common Date formats.
8
+ DateMatcher = / \A(?: (\w+,?\s+)?\w+\s+\d{1,2},?\s+\d{2,4} |
9
+ \d{4}-\d{2}-\d{2} )\z /x
10
+ # A Regexp used to find and convert some common DateTime formats.
11
+ DateTimeMatcher =
12
+ / \A(?: (\w+,?\s+)?\w+\s+\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2},?\s+\d{2,4} |
13
+ \d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2} )\z /x
14
+
15
+ many_to_one :project
16
+ plugin :serialization
17
+ serialize_attributes :marshal, :field_types, :field_names
18
+ mount_uploader :data, DataUploader
19
+
20
+ def data=(value)
21
+ result = super
22
+ self.name ||= File.basename(data.file.original_filename).sub(/\.\w+?$/, "").gsub(/[_-]+/, " ").capitalize
23
+ discover_fields
24
+ result
25
+ end
26
+
27
+ def primary_key_sym
28
+ primary_key_name.to_sym
29
+ end
30
+
31
+ def table_name
32
+ :"import_#{id}"
33
+ end
34
+
35
+ def preview
36
+ if @preview.nil?
37
+ @preview = []
38
+ FasterCSV.open(data.file.file) do |csv|
39
+ csv.rewind
40
+ csv.shift if self.has_headers
41
+ 50.times do |i|
42
+ row = csv.shift
43
+ break if row.nil?
44
+ @preview << row
45
+ end
46
+ end
47
+ end
48
+ @preview
49
+ end
50
+
51
+ def import!
52
+ project.local_database do |db|
53
+ column_info = []
54
+ column_names = []
55
+ column_types = []
56
+ field_names.each_with_index do |name, i|
57
+ name_sym = name.to_sym
58
+ column_names << name_sym
59
+ column_types << {
60
+ :name => name_sym,
61
+ :type =>
62
+ case field_types[i]
63
+ when 'integer' then Integer
64
+ when 'string' then String
65
+ end,
66
+ :null => !(name == primary_key_name)
67
+ }
68
+ end
69
+ column_names << :dup_key_count
70
+ column_types << {:name => :dup_key_count, :type => Integer}
71
+ db.create_table!(table_name) do
72
+ columns.push(*column_types)
73
+ end
74
+
75
+ ds = db[table_name]
76
+ key_frequencies = Hash.new { |h, k| h[k] = 0 }
77
+ buffer = ImportBuffer.new(column_names, ds)
78
+ skip = has_headers
79
+ primary_key_index = field_names.index(primary_key_name)
80
+ FasterCSV.foreach(data.file.file) do |row|
81
+ if skip
82
+ # skip header if necessary
83
+ skip = false
84
+ next
85
+ end
86
+
87
+ key = row[primary_key_index]
88
+ num = key_frequencies[key] += 1
89
+ row.push(num > 1 ? num : nil)
90
+ self.has_duplicate_keys = true if num > 1
91
+
92
+ buffer.add(row)
93
+ end
94
+ buffer.flush
95
+
96
+ primary_key = self.primary_key_sym
97
+ if has_duplicate_keys
98
+ # flag duplicate primary keys
99
+ key_frequencies.each_pair do |key, count|
100
+ next if count == 1
101
+ ds.filter(primary_key => key, :dup_key_count => nil).update(:dup_key_count => 1)
102
+ end
103
+ else
104
+ # alter table to set primary key
105
+ db.alter_table(table_name) do
106
+ drop_column(:dup_key_count)
107
+ add_primary_key([primary_key])
108
+ end
109
+ end
110
+ end
111
+ update(:occurred_at => Time.now)
112
+ !has_duplicate_keys
113
+ end
114
+
115
+ def dataset
116
+ project.local_database do |db|
117
+ yield(db[table_name])
118
+ end
119
+ end
120
+
121
+ def repair_duplicate_keys!(rows_to_remove = nil)
122
+ pkey = primary_key_sym
123
+ project.local_database do |db|
124
+ ds = db[table_name]
125
+ if rows_to_remove
126
+ filtered_ds = nil
127
+ rows_to_remove.each_pair do |key, dups|
128
+ hsh = {pkey => key, :dup_key_count => dups}
129
+ filtered_ds = filtered_ds ? filtered_ds.or(hsh) : ds.filter(hsh)
130
+ end
131
+ filtered_ds.delete if filtered_ds
132
+ end
133
+
134
+ # only reassign keys if there is more than 1 duplicate per key
135
+ keys = ds.group(pkey).having { count(pkey) > 1 }.select_map(pkey)
136
+
137
+ current_key = nil
138
+ next_key = ds.order(pkey).last[pkey].next
139
+ ds.filter(pkey => keys).order(:dup_key_count).each do |row|
140
+ # skip the first one, since it'll retain the key
141
+ if current_key != row[pkey]
142
+ current_key = row[pkey]
143
+ else
144
+ ds.filter(pkey => row[pkey], :dup_key_count => row[:dup_key_count]).
145
+ update(pkey => next_key)
146
+ next_key = next_key.next
147
+ end
148
+ end
149
+
150
+ db.alter_table(table_name) do
151
+ drop_column(:dup_key_count)
152
+ add_primary_key([pkey])
153
+ end
154
+ end
155
+ end
156
+
157
+ private
158
+ def discover_fields
159
+ FasterCSV.open(data.file.file) do |csv|
160
+ csv.rewind
161
+
162
+ count = 0
163
+ types = []
164
+ type_counts = []
165
+ headers = csv.shift
166
+ if headers.any? { |h| h !~ /[A-Za-z_$]/ }
167
+ row = headers
168
+ headers = nil
169
+ self.has_headers = false
170
+ else
171
+ self.has_headers = true
172
+ headers.each_with_index do |name, i|
173
+ if name =~ /^id$/i
174
+ self.primary_key_name = name
175
+ end
176
+ end
177
+ row = csv.shift
178
+ end
179
+
180
+ while row && count < 50
181
+ row.each_with_index do |value, i|
182
+ hash = type_counts[i] ||= {}
183
+ type =
184
+ case value
185
+ when /^\d+$/ then 'integer'
186
+ else 'string'
187
+ end
188
+ hash[type] = (hash[type] || 0) + 1
189
+ end
190
+ row = csv.shift
191
+ count += 1
192
+ end
193
+
194
+ type_counts.each_with_index do |type_count, i|
195
+ types[i] = type_count.max { |a, b| a[1] <=> b[1] }[0]
196
+ end
197
+
198
+ self.field_types = types
199
+ self.field_names = headers
200
+ end
201
+ end
202
+
203
+ def validate
204
+ super
205
+
206
+ validates_presence :project_id
207
+ if project_id
208
+ # don't allow import to have the same name as an already existing resource
209
+ if project.resources_dataset.filter(:name => name).count > 0
210
+ errors.add(:name, "is already taken")
211
+ end
212
+ end
213
+ validates_presence [:field_names, :primary_key_name]
214
+ if field_names.is_a?(Array)
215
+ validates_includes field_names, [:primary_key_name]
216
+
217
+ expected = field_types.length
218
+ if field_names.length != expected
219
+ errors.add(:field_names, "must be of length #{expected}")
220
+ end
221
+
222
+ # check for duplicate field names
223
+ duplicates = {}
224
+ field_names.inject(Hash.new(0)) do |hash, field_name|
225
+ num = hash[field_name] += 1
226
+ duplicates[field_name] = num if num > 1
227
+ hash
228
+ end
229
+ if !duplicates.empty?
230
+ message = "have duplicates (%s)" %
231
+ duplicates.inject("") { |s, (k, v)| s + "#{k} x #{v}, " }.chomp(", ")
232
+ errors.add(:field_names, message)
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end