coupler 0.0.1-java

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 (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