bespoke 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ module Bespoke
2
+ class Export
3
+ class Filter
4
+ class StringLiteral
5
+ def self.match?(string)
6
+ string.start_with?("'") && string.end_with?("'")
7
+ end
8
+
9
+ def self.extract_from(string)
10
+ unquoted_literal = string[1..-2]
11
+ Sequel.expr(unquoted_literal)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -11,7 +11,12 @@ module Bespoke
11
11
 
12
12
  def database
13
13
  return @database if @database
14
- @database = Sequel.sqlite
14
+ @database = Sequel.sqlite(nil, after_connect: ->(connection) {
15
+ connection.create_function('concat_ws', -1) {|proxy, *args|
16
+ separator = args.pop
17
+ proxy.result = args.compact.join(separator)
18
+ }
19
+ })
15
20
  create_tables
16
21
  @database
17
22
  end
@@ -27,9 +32,15 @@ module Bespoke
27
32
  end
28
33
  end
29
34
 
30
- def query(sql)
35
+ def [](sql)
31
36
  database[sql]
32
37
  end
38
+ alias_method :query, :[]
39
+
40
+ def projection_named(table_name)
41
+ table_symbol = database.tables.find{|t| t.to_s == table_name.to_s}
42
+ table_symbol.nil? ? nil : database[table_symbol]
43
+ end
33
44
 
34
45
  protected
35
46
 
@@ -39,17 +50,13 @@ module Bespoke
39
50
 
40
51
  def create_table(prj)
41
52
  database.drop_table?(prj.name)
53
+ prj.ensure_primary_key!
42
54
  database.create_table(prj.name) do
43
55
  prj.fields.each do |field|
44
- if field.name.to_s == "ref_id"
45
- column :ref_id, :string, :primary_key => true
46
- else
47
- column field.name, field.type
48
- end
56
+ column field.name, field.type, field.column_options
49
57
  end
50
58
  end
51
59
  end
52
-
53
60
  end
54
61
  end
55
62
 
@@ -0,0 +1,17 @@
1
+ require 'mustache'
2
+
3
+ if RUBY_VERSION >= '2.0'
4
+ class Mustache
5
+ TABLE_FOR_ESCAPE_HTML = {
6
+ '&' => '&',
7
+ '"' => '"',
8
+ '<' => '&lt;',
9
+ '>' => '&gt;',
10
+ }
11
+ REGEXP_FOR_ESCAPE_HTML = Regexp.new("[#{ TABLE_FOR_ESCAPE_HTML.keys.map{|k| Regexp.escape(k) }.join }]")
12
+
13
+ def escapeHTML(string)
14
+ string.gsub(REGEXP_FOR_ESCAPE_HTML, TABLE_FOR_ESCAPE_HTML)
15
+ end
16
+ end
17
+ end
@@ -1,28 +1,20 @@
1
- module Bespoke
2
- class TypedField
3
- attr_reader :name, :type, :xpath
4
-
5
- # @name The name of the column or field
6
- # @type The Sqlite3 type of the data
7
- # @xpath The xpath query that retrieves the data from XML
8
- def initialize(name, type, xpath)
9
- @name = name
10
- @type = type
11
- @xpath = xpath
12
- end
13
- end
1
+ require_relative 'typed_field'
14
2
 
3
+ module Bespoke
15
4
  class Projection
16
5
  attr_reader :name, :fields, :xpath_root
17
6
 
18
- def initialize(name, xpath_root=nil)
7
+ class NoPrimaryKeyError < Bespoke::Error; end
8
+
9
+ def initialize(name, xpath_root=nil, &block)
19
10
  @name = name.to_s
20
11
  @xpath_root = xpath_root
21
12
  @fields = []
13
+ handle_init_block(&block) if block_given?
22
14
  end
23
15
 
24
- def field(name, type=:string, xpath=nil)
25
- @fields << TypedField.new(name, type, xpath)
16
+ def field(name, type=:string, options_or_xpath = {})
17
+ @fields << TypedField.new(name, type, options_or_xpath)
26
18
  end
27
19
 
28
20
  def xpath_hash
@@ -31,5 +23,27 @@ module Bespoke
31
23
  end.flatten(1)
32
24
  Hash[*pairs]
33
25
  end
26
+
27
+ def ensure_primary_key!
28
+ return if @fields.any?(&:primary_key?)
29
+
30
+ ref_id = @fields.find{|f| f.name == :ref_id }
31
+ if ref_id
32
+ warn 'Implicit primary key of ref_id detected, this feature is deprecated and will be removed soon.'
33
+ ref_id.make_primary_key!
34
+ else
35
+ raise NoPrimaryKeyError, "No primary key specified or implied for projection #{ self.name }"
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def handle_init_block(&block)
42
+ if block.arity == 1
43
+ block.call(self)
44
+ else
45
+ instance_exec(&block)
46
+ end
47
+ end
34
48
  end
35
- end
49
+ end
@@ -1,4 +1,4 @@
1
- require 'mustache'
1
+ require_relative './mustache'
2
2
  require 'tempfile'
3
3
 
4
4
  module Bespoke
@@ -42,4 +42,4 @@ module Bespoke
42
42
  end
43
43
  end
44
44
  end
45
- end
45
+ end
@@ -0,0 +1,30 @@
1
+ module Bespoke
2
+ class TypedField
3
+ attr_reader :name, :type, :column_options, :xpath
4
+
5
+ # @name The name of the column or field
6
+ # @type The Sqlite3 type of the data
7
+ # @options_or_xpath The options to be supplied to Sequel when generating the column
8
+ # along with the xpath used to extract the data from XML
9
+ def initialize(name, type, options_or_xpath = {})
10
+ @name = name
11
+ @type = type
12
+ if options_or_xpath.is_a? Hash
13
+ @xpath = options_or_xpath.delete(:xpath)
14
+ @column_options = options_or_xpath
15
+ else
16
+ @xpath = options_or_xpath
17
+ @column_options = {}
18
+ warn "Passing a string or array as the xpath is deprecated, please supply an options hash with an xpath key at: #{ caller[1] }"
19
+ end
20
+ end
21
+
22
+ def make_primary_key!
23
+ column_options[:primary_key] = true
24
+ end
25
+
26
+ def primary_key?
27
+ !!column_options[:primary_key]
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module Bespoke
2
- VERSION = '0.2.0' unless defined?(Bespoke::VERSION)
2
+ VERSION = '0.2.1' unless defined?(Bespoke::VERSION)
3
3
  end
@@ -98,4 +98,4 @@
98
98
  </xsl:choose>
99
99
  </xsl:template>
100
100
 
101
- </xsl:stylesheet>
101
+ </xsl:stylesheet>
@@ -82,4 +82,4 @@
82
82
  </xsl:choose>
83
83
  </xsl:template>
84
84
 
85
- </xsl:stylesheet>
85
+ </xsl:stylesheet>
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ module Bespoke
4
+ class Export::Filter
5
+ describe FunctionCall do
6
+ describe '#function' do
7
+ it 'must correctly identify the function specified when no args are present' do
8
+ call = FunctionCall.new('rand()')
9
+ expect(call.function).to eq 'rand'
10
+ end
11
+
12
+ it 'must correctly identify the function specified when an argument is being passed' do
13
+ call = FunctionCall.new("foo_bar(0)")
14
+ expect(call.function).to eq 'foo_bar'
15
+ end
16
+
17
+ it 'must correctly identify the function specified when multiple arguments are being supplied' do
18
+ call = FunctionCall.new("foo_bar(0, 2, foo)")
19
+ expect(call.function).to eq 'foo_bar'
20
+ end
21
+ end
22
+
23
+ describe '#arguments' do
24
+ it 'must return an empty array when no arguments are specified for the function being called' do
25
+ call = FunctionCall.new('rand()')
26
+ expect(call.arguments).to eq []
27
+ end
28
+
29
+ it 'must return an array with a single string when a single argument is supplied' do
30
+ call = FunctionCall.new("foo_bar(0)")
31
+ expect(call.arguments).to eq ['0']
32
+ end
33
+
34
+ it 'must return an array with the specified values when multiple values are supplied' do
35
+ call = FunctionCall.new("foo_bar(0, foo, bar)")
36
+ expect(call.arguments).to eq %w{0 foo bar}
37
+ end
38
+
39
+ it 'must reutrn the correct values when the whitespace is inconsistent' do
40
+ call = FunctionCall.new("foo_bar(\t 0, foo,bar )")
41
+ expect(call.arguments).to eq %w{0 foo bar}
42
+ end
43
+ end
44
+
45
+ describe '#extract' do
46
+ it 'must return a Sequel function node for most inputs' do
47
+ call = FunctionCall.new("foo_bar(0, foo, bar)")
48
+ extracted_call = call.extract
49
+ expect(extracted_call).to be_a Sequel::SQL::Function
50
+ expect(extracted_call.name).to eq 'FOO_BAR'
51
+ expect(extracted_call.args).to eq %w{0 foo bar}
52
+ end
53
+
54
+ it 'must specially handle a function named random_str' do
55
+ call = FunctionCall.new("random_str(4)")
56
+ extracted_call = call.extract
57
+ expect(extracted_call).to be_a Sequel::LiteralString
58
+ expect(extracted_call.to_s).to eq 'LOWER(HEX(RANDOMBLOB(4)))'
59
+ end
60
+ end
61
+
62
+ describe "custom functions" do
63
+ it "can be created" do
64
+ call = FunctionCall.new("myfn('hi')", :myfn => ->(args) { "LOWER(#{args.first})" })
65
+ extracted_call = call.extract
66
+ expect(extracted_call).to be_a Sequel::LiteralString
67
+ expect(extracted_call.to_s).to eq "LOWER('hi')"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+ require 'bespoke/export/filter/identifier'
3
+
4
+ module Bespoke
5
+ class Export::Filter
6
+ describe Identifier do
7
+ describe '#sequel_identifier' do
8
+ it 'must return an unqualified identifer with the right content for an unqualified column' do
9
+ identifier = Identifier.new('id')
10
+ sequel_identifier = identifier.sequel_identifier
11
+ expect(sequel_identifier).to be_a Sequel::SQL::Identifier
12
+ expect(sequel_identifier.value).to eq 'id'
13
+ end
14
+
15
+ it 'must return a qualified identifier with the right conent for a qualified column' do
16
+ identifier = Identifier.new('students.id')
17
+ sequel_identifier = identifier.sequel_identifier
18
+ expect(sequel_identifier).to be_a Sequel::SQL::QualifiedIdentifier
19
+ expect(sequel_identifier.table).to eq 'students'
20
+ expect(sequel_identifier.column).to eq 'id'
21
+ end
22
+ end
23
+
24
+ describe '#parts' do
25
+ it 'must correctly handle identifiers surrounded by arbitrary whitespace' do
26
+ identifier = Identifier.new("\t students.id ")
27
+ expect(identifier.parts).to eq({table: 'students', column: 'id'})
28
+ end
29
+
30
+ it 'must take an unqualified unquoted column identifier and return the correct values' do
31
+ identifier = Identifier.new('id')
32
+ expect(identifier.parts).to eq({table: nil, column: 'id'})
33
+ end
34
+
35
+ it 'must take a qualified unquoted column identifier and return the correct values' do
36
+ identifier = Identifier.new('students.id')
37
+ expect(identifier.parts).to eq({table: 'students', column: 'id'})
38
+ end
39
+
40
+ it 'must take an unqualified quoted column identifier and return the correct values' do
41
+ identifier = Identifier.new('"name.first_name"')
42
+ expect(identifier.parts).to eq({table: nil, column: 'name.first_name'})
43
+ end
44
+
45
+ it 'must take a qualified column identifier with the column name quoted and return the correct values' do
46
+ identifier = Identifier.new('students."name.first_name"')
47
+ expect(identifier.parts).to eq({table: 'students', column: 'name.first_name'})
48
+ end
49
+
50
+ it 'must take a qualified column identifier with the table name quoted and return the correct values' do
51
+ identifier = Identifier.new('"students".name')
52
+ expect(identifier.parts).to eq({table: 'students', column: 'name'})
53
+ end
54
+
55
+ it 'must take a qualified, fully quoted column identifier and return the correct values' do
56
+ identifier = Identifier.new('"students"."name.first_name"')
57
+ expect(identifier.parts).to eq({table: 'students', column: 'name.first_name'})
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ module Bespoke
4
+ class Export::Filter
5
+ describe SelectTemplate do
6
+ let(:join) { basic_join }
7
+ let(:db) { join.database }
8
+ let(:table) { db[:two] }
9
+ let(:template) { SelectTemplate.new(:course_id, '') }
10
+
11
+ describe '#compile' do
12
+ let(:result) { template.compile }
13
+ let(:sql) { table.select(result).sql }
14
+
15
+ it 'must build an aliased expression with the correct alias' do
16
+ expect(result.aliaz).to eq :course_id
17
+ end
18
+
19
+ it 'must interpret a symbol as a column specification' do
20
+ template.template_string = :id
21
+ expect(sql).to include %q[`id` AS 'course_id']
22
+ end
23
+
24
+ it 'must interpret a symbol with double underscores as a qualified column specification' do
25
+ template.template_string = :sections__id
26
+ expect(sql).to include %q[`sections`.`id` AS 'course_id']
27
+ end
28
+
29
+ it 'must interpret unqualified templates correctly' do
30
+ template.template_string = '<id>'
31
+ expect(sql).to include %q[`id` AS 'course_id']
32
+ end
33
+
34
+ it 'must interpret qualified templates correctly' do
35
+ template.template_string = '<sections.id>'
36
+ expect(sql).to include %q[`sections`.`id` AS 'course_id']
37
+ end
38
+
39
+ it 'must interpret a template with multiple values as neeing to be caalesced' do
40
+ template.template_string = '<id | foo>'
41
+ expect(sql).to include %q[COALESCE(`id`, `foo`) AS 'course_id']
42
+ end
43
+
44
+ it 'must interepret a template in single quotes as a literal value' do
45
+ template.template_string = "<'foo'>"
46
+ expect(sql).to include %q['foo' AS 'course_id']
47
+ end
48
+
49
+ it 'must coalesce a identifer and a literal string correctly' do
50
+ template.template_string = "<id | 'foo'>"
51
+ expect(sql).to include %q[COALESCE(`id`, 'foo') AS 'course_id']
52
+ end
53
+
54
+ it 'must interepret a default value of random_str(\d+) correctly' do
55
+ template.template_string = '<courses.enrollment_count | random_str(6)>'
56
+ expect(sql).to include %q[COALESCE(`courses`.`enrollment_count`, LOWER(HEX(RANDOMBLOB(6)))) AS 'course_id']
57
+ end
58
+
59
+ it 'must interpret a template consisting of a column and a prepended string correctly' do
60
+ template.template_string = 'course_<courses.id>'
61
+ expect(sql).to include %q[('course_' || `courses`.`id`) AS 'course_id']
62
+ end
63
+
64
+ it 'must interpret a template consisting of a columna and an appended string correctly' do
65
+ template.template_string = '<students.student_username>@foo.k12.mo.us'
66
+ expect(sql).to include %q[(`students`.`student_username` || '@foo.k12.mo.us') AS 'course_id']
67
+ end
68
+
69
+ it 'must interpret complex strings with column templates interpolated inside them' do
70
+ template.template_string = 'course_<courses.id>_record'
71
+ expect(sql).to include %q[('course_' || `courses`.`id` || '_record') AS 'course_id']
72
+ end
73
+
74
+ it 'must interpret 2 element joined array selecte template strings correctly' do
75
+ template.template_string = '<section_id>-<term_id>'
76
+ expect(sql).to include %q[concat_ws(`section_id`, `term_id`, '-') AS 'course_id']
77
+ end
78
+
79
+ it 'must interpret joined array select template strings correctly' do
80
+ template.template_string = '<section_id>-<term_id>-<staff_id>'
81
+ expect(sql).to include %q[concat_ws(`section_id`, `term_id`, `staff_id`, '-') AS 'course_id']
82
+ end
83
+
84
+ it 'must extract the correct separator from joined array template strings' do
85
+ template.template_string = '<section_id>=+=<term_id>=+=<staff_id>'
86
+ expect(sql).to include %q[concat_ws(`section_id`, `term_id`, `staff_id`, '=+=') AS 'course_id']
87
+ end
88
+
89
+ context "with custom function" do
90
+ let(:template) {
91
+ SelectTemplate.new(:course_id, '', :myfn => ->(args) { "MYFN()" })
92
+ }
93
+
94
+ it 'does it' do
95
+ template.template_string = '<myfn(5)>'
96
+ expect(sql).to include %q[MYFN()]
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end