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.
- data/.document +5 -0
- data/.gitmodules +3 -0
- data/.rvmrc +1 -0
- data/.vimrc +40 -0
- data/Gemfile +27 -0
- data/Gemfile.lock +71 -0
- data/LICENSE +20 -0
- data/NOTES +6 -0
- data/README.rdoc +18 -0
- data/Rakefile +42 -0
- data/TODO +11 -0
- data/VERSION +1 -0
- data/bin/coupler +7 -0
- data/db/.gitignore +6 -0
- data/db/migrate/001_initial_schema.rb +166 -0
- data/db/migrate/002_stub.rb +4 -0
- data/db/migrate/003_stub.rb +4 -0
- data/db/migrate/004_create_comparisons.rb +28 -0
- data/db/migrate/005_move_database_name.rb +19 -0
- data/db/migrate/006_upgrade_comparisons.rb +34 -0
- data/db/migrate/007_add_which_to_comparisons.rb +23 -0
- data/db/migrate/008_add_result_field_to_transformations.rb +33 -0
- data/db/migrate/009_add_generated_flag_to_fields.rb +13 -0
- data/db/migrate/010_create_imports.rb +24 -0
- data/db/migrate/011_add_primary_key_type.rb +13 -0
- data/db/migrate/012_add_transformed_with_to_resources.rb +13 -0
- data/db/migrate/013_add_run_count_to_scenarios.rb +13 -0
- data/db/migrate/014_add_last_accessed_at_to_some_tables.rb +13 -0
- data/db/migrate/015_add_run_number_to_results.rb +15 -0
- data/db/migrate/016_fix_scenario_run_count.rb +27 -0
- data/db/migrate/017_rename_comparison_columns.rb +14 -0
- data/db/migrate/018_fix_scenario_linkage_type.rb +8 -0
- data/db/migrate/019_add_columns_to_imports.rb +24 -0
- data/db/migrate/020_rename_import_columns.rb +12 -0
- data/db/migrate/021_add_fields_to_connections.rb +15 -0
- data/db/migrate/022_remove_database_name_from_resources.rb +11 -0
- data/features/connections.feature +28 -0
- data/features/matchers.feature +35 -0
- data/features/projects.feature +11 -0
- data/features/resources.feature +62 -0
- data/features/scenarios.feature +45 -0
- data/features/step_definitions/coupler_steps.rb +145 -0
- data/features/step_definitions/matchers_steps.rb +26 -0
- data/features/step_definitions/resources_steps.rb +12 -0
- data/features/step_definitions/scenarios_steps.rb +7 -0
- data/features/step_definitions/transformations_steps.rb +3 -0
- data/features/support/env.rb +128 -0
- data/features/transformations.feature +22 -0
- data/features/wizard.feature +10 -0
- data/gfx/coupler-header.svg +213 -0
- data/gfx/coupler-sidebar.svg +656 -0
- data/gfx/coupler.svg +184 -0
- data/gfx/icon.svg +75 -0
- data/lib/coupler/base.rb +63 -0
- data/lib/coupler/config.rb +128 -0
- data/lib/coupler/data_uploader.rb +20 -0
- data/lib/coupler/database.rb +31 -0
- data/lib/coupler/extensions/connections.rb +57 -0
- data/lib/coupler/extensions/exceptions.rb +58 -0
- data/lib/coupler/extensions/imports.rb +43 -0
- data/lib/coupler/extensions/jobs.rb +21 -0
- data/lib/coupler/extensions/matchers.rb +64 -0
- data/lib/coupler/extensions/projects.rb +62 -0
- data/lib/coupler/extensions/resources.rb +89 -0
- data/lib/coupler/extensions/results.rb +100 -0
- data/lib/coupler/extensions/scenarios.rb +50 -0
- data/lib/coupler/extensions/transformations.rb +70 -0
- data/lib/coupler/extensions/transformers.rb +58 -0
- data/lib/coupler/extensions.rb +16 -0
- data/lib/coupler/helpers.rb +121 -0
- data/lib/coupler/import_buffer.rb +48 -0
- data/lib/coupler/logger.rb +16 -0
- data/lib/coupler/models/common_model.rb +104 -0
- data/lib/coupler/models/comparison.rb +166 -0
- data/lib/coupler/models/connection.rb +59 -0
- data/lib/coupler/models/field.rb +55 -0
- data/lib/coupler/models/import.rb +238 -0
- data/lib/coupler/models/job.rb +42 -0
- data/lib/coupler/models/jobify.rb +17 -0
- data/lib/coupler/models/matcher.rb +36 -0
- data/lib/coupler/models/project.rb +40 -0
- data/lib/coupler/models/resource.rb +287 -0
- data/lib/coupler/models/result.rb +92 -0
- data/lib/coupler/models/scenario/runner.rb +357 -0
- data/lib/coupler/models/scenario.rb +115 -0
- data/lib/coupler/models/transformation.rb +117 -0
- data/lib/coupler/models/transformer/runner.rb +28 -0
- data/lib/coupler/models/transformer.rb +110 -0
- data/lib/coupler/models.rb +30 -0
- data/lib/coupler/runner.rb +76 -0
- data/lib/coupler/scheduler.rb +56 -0
- data/lib/coupler.rb +34 -0
- data/log/.gitignore +1 -0
- data/misc/README +5 -0
- data/misc/jruby-json.license +57 -0
- data/misc/rack-flash.license +22 -0
- data/script/dbconsole.rb +5 -0
- data/src/edu/vanderbilt/coupler/Main.java +116 -0
- data/src/edu/vanderbilt/coupler/jruby.properties +1 -0
- data/tasks/annotations.rake +84 -0
- data/tasks/db.rake +120 -0
- data/tasks/environment.rake +12 -0
- data/tasks/jeweler.rake +43 -0
- data/tasks/package.rake +58 -0
- data/tasks/rdoc.rake +13 -0
- data/tasks/test.rake +63 -0
- data/tasks/vendor.rake +43 -0
- data/test/README.txt +6 -0
- data/test/config.yml +9 -0
- data/test/coupler/models/test_import.rb +221 -0
- data/test/factories.rb +91 -0
- data/test/fixtures/duplicate-keys.csv +5 -0
- data/test/fixtures/no-headers.csv +50 -0
- data/test/fixtures/people.csv +51 -0
- data/test/fixtures/varying-row-size.csv +4 -0
- data/test/helper.rb +156 -0
- data/test/integration/extensions/test_connections.rb +80 -0
- data/test/integration/extensions/test_imports.rb +94 -0
- data/test/integration/extensions/test_jobs.rb +52 -0
- data/test/integration/extensions/test_matchers.rb +134 -0
- data/test/integration/extensions/test_projects.rb +82 -0
- data/test/integration/extensions/test_resources.rb +150 -0
- data/test/integration/extensions/test_results.rb +89 -0
- data/test/integration/extensions/test_scenarios.rb +88 -0
- data/test/integration/extensions/test_transformations.rb +113 -0
- data/test/integration/extensions/test_transformers.rb +80 -0
- data/test/integration/test_field.rb +45 -0
- data/test/integration/test_import.rb +78 -0
- data/test/integration/test_running_scenarios.rb +379 -0
- data/test/integration/test_transformation.rb +56 -0
- data/test/integration/test_transforming.rb +154 -0
- data/test/table_sets.rb +76 -0
- data/test/unit/models/test_common_model.rb +130 -0
- data/test/unit/models/test_comparison.rb +619 -0
- data/test/unit/models/test_connection.rb +115 -0
- data/test/unit/models/test_field.rb +99 -0
- data/test/unit/models/test_import.rb +130 -0
- data/test/unit/models/test_job.rb +115 -0
- data/test/unit/models/test_matcher.rb +82 -0
- data/test/unit/models/test_project.rb +102 -0
- data/test/unit/models/test_resource.rb +564 -0
- data/test/unit/models/test_result.rb +90 -0
- data/test/unit/models/test_scenario.rb +199 -0
- data/test/unit/models/test_transformation.rb +193 -0
- data/test/unit/models/test_transformer.rb +188 -0
- data/test/unit/test_base.rb +60 -0
- data/test/unit/test_data_uploader.rb +27 -0
- data/test/unit/test_database.rb +23 -0
- data/test/unit/test_helpers.rb +58 -0
- data/test/unit/test_logger.rb +10 -0
- data/test/unit/test_models.rb +12 -0
- data/test/unit/test_runner.rb +76 -0
- data/test/unit/test_scheduler.rb +66 -0
- data/uploads/.gitignore +2 -0
- data/vendor/java/.gitignore +5 -0
- data/webroot/public/css/960.css +1 -0
- data/webroot/public/css/dataTables.css +1057 -0
- data/webroot/public/css/jquery-ui.css +572 -0
- data/webroot/public/css/jquery.treeview.css +68 -0
- data/webroot/public/css/reset.css +1 -0
- data/webroot/public/css/style.css +504 -0
- data/webroot/public/css/text.css +1 -0
- data/webroot/public/favicon.ico +0 -0
- data/webroot/public/images/12_col.gif +0 -0
- data/webroot/public/images/16_col.gif +0 -0
- data/webroot/public/images/add.png +0 -0
- data/webroot/public/images/ajax-loader.gif +0 -0
- data/webroot/public/images/cog.png +0 -0
- data/webroot/public/images/coupler.png +0 -0
- data/webroot/public/images/foo.png +0 -0
- data/webroot/public/images/hammer.png +0 -0
- data/webroot/public/images/header.png +0 -0
- data/webroot/public/images/home.gif +0 -0
- data/webroot/public/images/jobs.gif +0 -0
- data/webroot/public/images/sidebar-bottom.png +0 -0
- data/webroot/public/images/sidebar.png +0 -0
- data/webroot/public/images/treeview-default-line.gif +0 -0
- data/webroot/public/images/treeview-default.gif +0 -0
- data/webroot/public/images/ui-anim_basic_16x16.gif +0 -0
- data/webroot/public/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/webroot/public/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/webroot/public/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/webroot/public/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/webroot/public/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/webroot/public/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/webroot/public/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/webroot/public/images/ui-bg_highlight-hard_30_565356_1x100.png +0 -0
- data/webroot/public/images/ui-bg_highlight-hard_75_888588_1x100.png +0 -0
- data/webroot/public/images/ui-bg_highlight-soft_30_6e3b3a_1x100.png +0 -0
- data/webroot/public/images/ui-bg_highlight-soft_35_8e8b8e_1x100.png +0 -0
- data/webroot/public/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/webroot/public/images/ui-icons_222222_256x240.png +0 -0
- data/webroot/public/images/ui-icons_2e83ff_256x240.png +0 -0
- data/webroot/public/images/ui-icons_454545_256x240.png +0 -0
- data/webroot/public/images/ui-icons_888888_256x240.png +0 -0
- data/webroot/public/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/webroot/public/images/ui-icons_ffffff_256x240.png +0 -0
- data/webroot/public/js/ajaxupload.js +673 -0
- data/webroot/public/js/application.js +40 -0
- data/webroot/public/js/jquery-ui.combobox.js +98 -0
- data/webroot/public/js/jquery-ui.js +9867 -0
- data/webroot/public/js/jquery-ui.min.js +559 -0
- data/webroot/public/js/jquery.dataTables.min.js +587 -0
- data/webroot/public/js/jquery.min.js +154 -0
- data/webroot/public/js/jquery.timeago.js +140 -0
- data/webroot/public/js/jquery.tooltip.min.js +19 -0
- data/webroot/public/js/jquery.treeview.min.js +15 -0
- data/webroot/public/js/resource.js +11 -0
- data/webroot/public/js/results.js +56 -0
- data/webroot/public/js/transformations.js +95 -0
- data/webroot/views/connections/index.erb +5 -0
- data/webroot/views/connections/list.erb +34 -0
- data/webroot/views/connections/new.erb +55 -0
- data/webroot/views/connections/show.erb +36 -0
- data/webroot/views/imports/edit.erb +60 -0
- data/webroot/views/imports/form.erb +81 -0
- data/webroot/views/imports/new.erb +89 -0
- data/webroot/views/index.erb +12 -0
- data/webroot/views/jobs/index.erb +7 -0
- data/webroot/views/jobs/list.erb +24 -0
- data/webroot/views/layout.erb +38 -0
- data/webroot/views/matchers/form.erb +250 -0
- data/webroot/views/matchers/list.erb +32 -0
- data/webroot/views/projects/form.erb +14 -0
- data/webroot/views/projects/index.erb +96 -0
- data/webroot/views/projects/show.erb +24 -0
- data/webroot/views/resources/edit.erb +88 -0
- data/webroot/views/resources/index.erb +5 -0
- data/webroot/views/resources/list.erb +27 -0
- data/webroot/views/resources/new.erb +121 -0
- data/webroot/views/resources/show.erb +86 -0
- data/webroot/views/resources/transform.erb +2 -0
- data/webroot/views/results/csv.erb +12 -0
- data/webroot/views/results/details.erb +15 -0
- data/webroot/views/results/index.erb +2 -0
- data/webroot/views/results/list.erb +22 -0
- data/webroot/views/results/record.erb +24 -0
- data/webroot/views/results/show.erb +68 -0
- data/webroot/views/scenarios/index.erb +5 -0
- data/webroot/views/scenarios/list.erb +20 -0
- data/webroot/views/scenarios/new.erb +99 -0
- data/webroot/views/scenarios/run.erb +2 -0
- data/webroot/views/scenarios/show.erb +50 -0
- data/webroot/views/sidebar.erb +106 -0
- data/webroot/views/transformations/create.erb +115 -0
- data/webroot/views/transformations/for.erb +16 -0
- data/webroot/views/transformations/index.erb +2 -0
- data/webroot/views/transformations/list.erb +29 -0
- data/webroot/views/transformations/new.erb +126 -0
- data/webroot/views/transformations/preview.erb +46 -0
- data/webroot/views/transformers/edit.erb +6 -0
- data/webroot/views/transformers/form.erb +58 -0
- data/webroot/views/transformers/index.erb +2 -0
- data/webroot/views/transformers/list.erb +25 -0
- data/webroot/views/transformers/new.erb +5 -0
- data/webroot/views/transformers/preview.erb +23 -0
- data/webroot/views/transformers/show.erb +0 -0
- 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
|