querybuilder 0.5.9 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/History.txt +29 -25
  2. data/Manifest.txt +20 -9
  3. data/README.rdoc +73 -10
  4. data/Rakefile +62 -30
  5. data/lib/extconf.rb +3 -0
  6. data/lib/query_builder.rb +39 -898
  7. data/lib/query_builder/error.rb +7 -0
  8. data/lib/query_builder/info.rb +3 -0
  9. data/lib/query_builder/parser.rb +80 -0
  10. data/lib/query_builder/processor.rb +714 -0
  11. data/lib/query_builder/query.rb +273 -0
  12. data/lib/querybuilder_ext.c +1870 -0
  13. data/lib/querybuilder_ext.rl +418 -0
  14. data/lib/querybuilder_rb.rb +1686 -0
  15. data/lib/querybuilder_rb.rl +214 -0
  16. data/lib/querybuilder_syntax.rl +47 -0
  17. data/old_QueryBuilder.rb +946 -0
  18. data/querybuilder.gemspec +42 -15
  19. data/tasks/build.rake +20 -0
  20. data/test/dummy_test.rb +21 -0
  21. data/test/mock/custom_queries/test.yml +5 -4
  22. data/test/mock/dummy.rb +9 -0
  23. data/test/mock/dummy_processor.rb +160 -0
  24. data/test/mock/queries/bar.yml +1 -1
  25. data/test/mock/queries/foo.yml +2 -2
  26. data/test/mock/user_processor.rb +34 -0
  27. data/test/query_test.rb +38 -0
  28. data/test/querybuilder/basic.yml +91 -0
  29. data/test/{query_builder → querybuilder}/custom.yml +11 -11
  30. data/test/querybuilder/errors.yml +32 -0
  31. data/test/querybuilder/filters.yml +115 -0
  32. data/test/querybuilder/group.yml +7 -0
  33. data/test/querybuilder/joins.yml +37 -0
  34. data/test/querybuilder/mixed.yml +18 -0
  35. data/test/querybuilder/rubyless.yml +15 -0
  36. data/test/querybuilder_test.rb +111 -0
  37. data/test/test_helper.rb +8 -3
  38. metadata +66 -19
  39. data/test/mock/dummy_query.rb +0 -114
  40. data/test/mock/user_query.rb +0 -55
  41. data/test/query_builder/basic.yml +0 -60
  42. data/test/query_builder/errors.yml +0 -50
  43. data/test/query_builder/filters.yml +0 -43
  44. data/test/query_builder/joins.yml +0 -25
  45. data/test/query_builder/mixed.yml +0 -12
  46. data/test/query_builder_test.rb +0 -36
data/querybuilder.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{querybuilder}
8
- s.version = "0.5.9"
8
+ s.version = "0.7.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Gaspard Bucher"]
12
- s.date = %q{2010-08-30}
12
+ s.date = %q{2010-05-27}
13
13
  s.description = %q{QueryBuilder is an interpreter for the "pseudo sql" language. This language
14
14
  can be used for two purposes:
15
15
 
@@ -25,35 +25,56 @@ Gem::Specification.new do |s|
25
25
  "Manifest.txt",
26
26
  "README.rdoc",
27
27
  "Rakefile",
28
+ "lib/extconf.rb",
28
29
  "lib/query_builder.rb",
30
+ "lib/query_builder/error.rb",
31
+ "lib/query_builder/info.rb",
32
+ "lib/query_builder/parser.rb",
33
+ "lib/query_builder/processor.rb",
34
+ "lib/query_builder/query.rb",
29
35
  "lib/querybuilder.rb",
36
+ "lib/querybuilder_ext.c",
37
+ "lib/querybuilder_ext.rl",
38
+ "lib/querybuilder_rb.rb",
39
+ "lib/querybuilder_rb.rl",
40
+ "lib/querybuilder_syntax.rl",
41
+ "old_QueryBuilder.rb",
30
42
  "querybuilder.gemspec",
31
43
  "script/console",
32
44
  "script/destroy",
33
45
  "script/generate",
46
+ "tasks/build.rake",
47
+ "test/dummy_test.rb",
34
48
  "test/mock/custom_queries/test.yml",
35
- "test/mock/dummy_query.rb",
49
+ "test/mock/dummy.rb",
50
+ "test/mock/dummy_processor.rb",
36
51
  "test/mock/queries/bar.yml",
37
52
  "test/mock/queries/foo.yml",
38
- "test/mock/user_query.rb",
39
- "test/query_builder/basic.yml",
40
- "test/query_builder/custom.yml",
41
- "test/query_builder/errors.yml",
42
- "test/query_builder/filters.yml",
43
- "test/query_builder/joins.yml",
44
- "test/query_builder/mixed.yml",
45
- "test/query_builder_test.rb",
53
+ "test/mock/user_processor.rb",
54
+ "test/query_test.rb",
55
+ "test/querybuilder/basic.yml",
56
+ "test/querybuilder/custom.yml",
57
+ "test/querybuilder/errors.yml",
58
+ "test/querybuilder/filters.yml",
59
+ "test/querybuilder/group.yml",
60
+ "test/querybuilder/joins.yml",
61
+ "test/querybuilder/mixed.yml",
62
+ "test/querybuilder/rubyless.yml",
63
+ "test/querybuilder_test.rb",
46
64
  "test/test_helper.rb"
47
65
  ]
48
66
  s.homepage = %q{http://zenadmin.org/524}
49
67
  s.rdoc_options = ["--charset=UTF-8"]
50
68
  s.require_paths = ["lib"]
51
69
  s.rubygems_version = %q{1.3.6}
52
- s.summary = %q{QueryBuilder is an interpreter for the "pseudo sql" language}
70
+ s.summary = %q{QueryBuilder is an interpreter for the "pseudo sql" language.}
53
71
  s.test_files = [
54
- "test/mock/dummy_query.rb",
55
- "test/mock/user_query.rb",
56
- "test/query_builder_test.rb",
72
+ "test/dummy_test.rb",
73
+ "test/mock/dummy.rb",
74
+ "test/mock/dummy_processor.rb",
75
+ "test/mock/user_processor.rb",
76
+ "test/query_test.rb",
77
+ "test/querybuilder_test.rb",
57
78
  "test/test_helper.rb"
58
79
  ]
59
80
 
@@ -62,11 +83,17 @@ Gem::Specification.new do |s|
62
83
  s.specification_version = 3
63
84
 
64
85
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
86
+ s.add_runtime_dependency(%q<rubyless>, [">= 0.5.0"])
87
+ s.add_development_dependency(%q<shoulda>, [">= 0"])
65
88
  s.add_development_dependency(%q<yamltest>, [">= 0.5.0"])
66
89
  else
90
+ s.add_dependency(%q<rubyless>, [">= 0.5.0"])
91
+ s.add_dependency(%q<shoulda>, [">= 0"])
67
92
  s.add_dependency(%q<yamltest>, [">= 0.5.0"])
68
93
  end
69
94
  else
95
+ s.add_dependency(%q<rubyless>, [">= 0.5.0"])
96
+ s.add_dependency(%q<shoulda>, [">= 0"])
70
97
  s.add_dependency(%q<yamltest>, [">= 0.5.0"])
71
98
  end
72
99
  end
data/tasks/build.rake ADDED
@@ -0,0 +1,20 @@
1
+
2
+ desc "Build native extension"
3
+ task :build => [:build_rb, :build_ext] do
4
+ end
5
+
6
+ task :build_rb do
7
+ `cd lib && ragel -R querybuilder_rb.rl`
8
+ end
9
+
10
+ task :build_ext => [:ragel_ext, :extconf] do
11
+ `cd lib && make`
12
+ end
13
+
14
+ task :extconf do
15
+ `cd lib && ruby extconf.rb`
16
+ end
17
+
18
+ task :ragel_ext do
19
+ `cd lib && ragel querybuilder_ext.rl`
20
+ end
@@ -0,0 +1,21 @@
1
+ require 'test_helper'
2
+
3
+ class DummyTest < Test::Unit::TestCase
4
+ Query = QueryBuilder::Query
5
+
6
+ context 'A class with QueryBuilder included' do
7
+
8
+ subject do
9
+ Dummy
10
+ end
11
+
12
+ should 'return compiler class on query_compiler' do
13
+ assert_equal DummyProcessor, subject.query_compiler
14
+ end
15
+
16
+ should 'return a Query object on build_query' do
17
+ assert_kind_of Query, subject.build_query(:all, 'objects')
18
+ end
19
+
20
+ end
21
+ end
@@ -1,4 +1,4 @@
1
- DummyQuery:
1
+ DummyProcessor:
2
2
  abc:
3
3
  select:
4
4
  - a
@@ -13,13 +13,14 @@ DummyQuery:
13
13
  order: a ASC
14
14
 
15
15
  two_table:
16
+ main_table: 'table_one'
16
17
  select:
17
- - x
18
+ - x AS x
18
19
  - IF(table_one.y,table_one.y,table_two.z) AS y
19
20
  - table_two.name
20
- tables:
21
- - table_one
21
+ tables:
22
22
  - table_two
23
+ - table_one
23
24
 
24
25
  two_table_main:
25
26
  main_table: foo
@@ -0,0 +1,9 @@
1
+ require 'mock/dummy_processor'
2
+
3
+ class Dummy
4
+ include RubyLess
5
+ safe_method :id => Number
6
+
7
+ include QueryBuilder
8
+ self.query_compiler = DummyProcessor
9
+ end
@@ -0,0 +1,160 @@
1
+ class DummyProcessor < QueryBuilder::Processor
2
+ set_main_table 'objects'
3
+ set_main_class 'DummyClass'
4
+ set_default :scope, 'self'
5
+ after_process :insert_after_filter
6
+ load_custom_queries File.join(File.dirname(__FILE__), '*')
7
+
8
+ # Scope current context with previous context.
9
+ # For example:
10
+ # current previous
11
+ # ['parent_id', 'id'] ==> no1.parent_id = nodes.id
12
+ def scope_fields(scope)
13
+ case scope
14
+ when 'self'
15
+ ['parent_id', 'id']
16
+ when 'parent'
17
+ last? ? ['parent_id', 'parent_id'] : ['parent_id', 'id']
18
+ when 'project'
19
+ last? ? ['project_id', 'project_id'] : ['project_id', 'id']
20
+ when 'site', main_table
21
+ # not an error, but do not scope
22
+ []
23
+ else
24
+ # error
25
+ nil
26
+ end
27
+ end
28
+
29
+ # Overwrite this and take care to check for valid fields.
30
+ def process_field(fld_name)
31
+ if ['id', 'parent_id', 'project_id', 'section_id', 'kpath', 'name', 'event_at', 'custom_a'].include?(fld_name)
32
+ "#{table}.#{fld_name}"
33
+ elsif fld_name == 'REF_DATE'
34
+ context[:ref_date] ? insert_bind(context[:ref_date]) : 'now()'
35
+ else
36
+ super # raises an error
37
+ end
38
+ end
39
+
40
+ # We do special things with 'class ='
41
+ def process_equal(left, right)
42
+ if left == [:field, 'class'] && right[0] == :string
43
+ case right.last
44
+ when 'Client'
45
+ kpath = 'NRCC'
46
+ else
47
+ raise QueryBuilder::SyntaxError.new("Unknown class #{right.last.inspect}.")
48
+ end
49
+ "#{field_or_attr('kpath')} LIKE #{insert_bind((kpath + '%').inspect)}"
50
+ else
51
+ super
52
+ end
53
+ end
54
+
55
+ # We do special things with 'class ='
56
+ def process_match(left, right)
57
+ end
58
+
59
+ def process_function(arg, method)
60
+ method, arg = process(method), process(arg)
61
+
62
+ case method
63
+ when 'year'
64
+ "strftime('%Y',#{arg})"
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ # ******** And maybe overwrite these **********
71
+ def parse_custom_query_argument(key, value)
72
+ return nil unless value
73
+ super(key, value.gsub('REF_DATE', context[:ref_date] ? insert_bind(context[:ref_date]) : 'now()'))
74
+ end
75
+
76
+ private
77
+ # Change class
78
+ def class_relation(relation)
79
+ case relation
80
+ when 'users'
81
+ change_processor 'UserProcessor'
82
+ add_table('users')
83
+ add_filter "#{table('users')}.node_id = #{field_or_attr('id', table(self.class.main_table))}"
84
+ return true
85
+ else
86
+ return nil
87
+ end
88
+ end
89
+
90
+ # Moving to another context without a join table
91
+ def context_relation(relation)
92
+ case relation
93
+ when 'self'
94
+ fields = ['id', 'id']
95
+ when 'parent'
96
+ fields = ['id', 'parent_id']
97
+ when 'project'
98
+ fields = ['id', 'project_id']
99
+ else
100
+ return nil
101
+ end
102
+
103
+ add_table(main_table)
104
+ add_filter "#{field_or_attr(fields[0])} = #{field_or_attr(fields[1], table(main_table, -1))}"
105
+ end
106
+
107
+ # Filtering of objects in scope
108
+ def filter_relation(relation)
109
+ case relation
110
+ when 'letters'
111
+ add_table(main_table)
112
+ add_filter "#{table}.kpath LIKE #{quote('NNL%')}"
113
+ when 'clients'
114
+ add_table(main_table)
115
+ add_filter "#{table}.kpath LIKE #{quote("NRCC%")}"
116
+ when main_table, 'children'
117
+ # no filter
118
+ add_table(main_table)
119
+ end
120
+ end
121
+
122
+ # Moving to another context through 'joins'
123
+ def join_relation(relation)
124
+ case relation
125
+ when 'recipients'
126
+ fields = ['source_id', 4, 'target_id']
127
+ when 'icons'
128
+ fields = ['target_id', 5, 'source_id']
129
+ when 'tags'
130
+ # just to test joins
131
+ add_table(main_table)
132
+ needs_join_table('objects', 'INNER', 'tags', 'TABLE1.id = TABLE2.node_id')
133
+ return true
134
+ else
135
+ return false
136
+ end
137
+
138
+ add_table(main_table)
139
+ add_table('links')
140
+ # source --> target
141
+ add_filter "#{table('links')}.#{fields[0]} = #{field_or_attr('id', table(main_table,-1))}"
142
+ add_filter "#{table('links')}.relation_id = #{fields[1]}"
143
+ add_filter "#{field_or_attr('id')} = #{table('links')}.#{fields[2]}"
144
+ end
145
+
146
+ def insert_after_filter
147
+ if after_filter = context[:after_filter]
148
+ add_filter after_filter
149
+ end
150
+ end
151
+ end
152
+
153
+
154
+ class DummyClass
155
+ # Mock connection and quoting
156
+ def self.connection; self; end
157
+ def self.quote(obj)
158
+ obj.kind_of?(String) ? "'#{obj}'" : obj
159
+ end
160
+ end
@@ -1,6 +1,6 @@
1
1
  group: test2
2
2
 
3
- DummyQuery:
3
+ DummyProcessor:
4
4
  bar:
5
5
  select:
6
6
  - bar
@@ -2,10 +2,10 @@ groups:
2
2
  - test
3
3
  - test2
4
4
 
5
- DummyQuery:
5
+ DummyProcessor:
6
6
  foo:
7
7
  select:
8
8
  - foo
9
9
  tables:
10
- - foot
10
+ - objects
11
11
  order: f DESC
@@ -0,0 +1,34 @@
1
+ class TestUser
2
+ def self.connection; self; end
3
+ def self.quote(obj)
4
+ obj.kind_of?(String) ? "'#{obj}'" : obj
5
+ end
6
+ end
7
+
8
+ class UserProcessor < QueryBuilder::Processor
9
+ set_main_table 'users'
10
+ set_main_class 'TestUser'
11
+ set_default :order, 'name asc, first_name asc'
12
+ set_default :context, 'self'
13
+
14
+ def process_relation(relation)
15
+ case relation
16
+ when 'objects'
17
+ this.apply_scope(default[:scope]) if context[:last]
18
+ add_table('objects')
19
+ @query.add_filter "#{table('objects')}.id = #{field_or_attr('node_id')}"
20
+ change_processor 'DummyProcessor'
21
+ else
22
+ return nil
23
+ end
24
+ end
25
+
26
+ # Overwrite this and take car to check for valid fields.
27
+ def process_field(field_name)
28
+ if ['id', 'name', 'first_name', 'node_id'].include?(field_name)
29
+ "#{table}.#{field_name}"
30
+ else
31
+ super # raises
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ require 'test_helper'
2
+
3
+ class QueryTest < Test::Unit::TestCase
4
+
5
+ context 'An empty query object' do
6
+ subject do
7
+ QueryBuilder::Query.new(QueryBuilder::Processor)
8
+ end
9
+
10
+ should 'respond to rebuild_tables' do
11
+ subject.tables = ['foo', 'bar']
12
+ subject.rebuild_tables!
13
+ h = {'foo' => ['foo'], 'bar' => ['bar']}
14
+ assert_equal h, subject.table_alias
15
+ end
16
+
17
+ should 'respond to rebuild_attributes_hash' do
18
+ subject.select = ['1 as one', 'two', '(20 - (weight / (height * height))) AS bmi_nrm']
19
+ subject.rebuild_attributes_hash!
20
+ h = {"bmi_nrm"=>"(20 - (weight / (height * height)))"}
21
+ assert_equal h, subject.attributes_alias
22
+ end
23
+ end
24
+
25
+ context 'A query returned from a processor' do
26
+ subject do
27
+ DummyProcessor.new('objects').query
28
+ end
29
+
30
+ should 'return a string representing an array with find SQL and parameters string on to_s' do
31
+ assert_equal "[%Q{SELECT objects.* FROM objects WHERE objects.parent_id = ?}, id]", subject.to_s
32
+ end
33
+
34
+ should 'return a string representing an array with count SQL and parameters string on to_s count' do
35
+ assert_equal "[%Q{SELECT COUNT(*) FROM objects WHERE objects.parent_id = ?}, id]", subject.to_s(:count)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,91 @@
1
+ empty:
2
+ src: ""
3
+ sxp: "[:query]"
4
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.parent_id = ?}, id]"
5
+
6
+ objects:
7
+ sxp: '[:query, [:relation, "objects"]]'
8
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.parent_id = ?}, id]"
9
+
10
+ objects_in_project:
11
+ sxp: '[:query, [:scope, [:relation, "objects"], "project"]]'
12
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.project_id = ?}, project_id]"
13
+
14
+ objects_in_site:
15
+ res: "%Q{SELECT objects.* FROM objects}"
16
+
17
+ recipients:
18
+ sxp: '[:query, [:relation, "recipients"]]'
19
+ res: "[%Q{SELECT objects.* FROM links,objects WHERE objects.id = links.target_id AND links.relation_id = 4 AND links.source_id = ?}, id]"
20
+ sql: "SELECT objects.* FROM links,objects WHERE objects.id = links.target_id AND links.relation_id = 4 AND links.source_id = 123"
21
+
22
+ letters_in_project:
23
+ sxp: '[:query, [:scope, [:relation, "letters"], "project"]]'
24
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.kpath LIKE 'NNL%' AND objects.project_id = ?}, project_id]"
25
+
26
+ parent:
27
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.id = ?}, parent_id]"
28
+
29
+ order_single_table:
30
+ src: "objects in project order by name ASC"
31
+ sxp: '[:query, [:order, [:scope, [:relation, "objects"], "project"], [:asc, [:field, "name"]]]]'
32
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.project_id = ? ORDER BY objects.name ASC}, project_id]"
33
+
34
+ order_many_tables:
35
+ src: "recipients order by name"
36
+ res: "[%Q{SELECT objects.* FROM links,objects WHERE objects.id = links.target_id AND links.relation_id = 4 AND links.source_id = ? ORDER BY objects.name}, id]"
37
+
38
+ order_many_params:
39
+ src: "objects in project order by name, id desc"
40
+ sxp: '[:query, [:order, [:scope, [:relation, "objects"], "project"], [:field, "name"], [:desc, [:field, "id"]]]]'
41
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.project_id = ? ORDER BY objects.name, objects.id DESC}, project_id]"
42
+
43
+ order_without_scope:
44
+ src: "objects order by name asc"
45
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.parent_id = ? ORDER BY objects.name ASC}, id]"
46
+
47
+ limit:
48
+ src: "objects in project limit 2"
49
+ sxp: '[:query, [:limit, [:scope, [:relation, "objects"], "project"], [:integer, "2"]]]'
50
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.project_id = ? LIMIT 2}, project_id]"
51
+
52
+ limit_with_order:
53
+ src: "objects in project order by name desc limit 10"
54
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.project_id = ? ORDER BY objects.name DESC LIMIT 10}, project_id]"
55
+
56
+ offset_in_limit:
57
+ src: "objects in project limit 3,2"
58
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.project_id = ? LIMIT 2 OFFSET 3}, project_id]"
59
+
60
+ offset:
61
+ src: "objects in project limit 2 offset 3"
62
+ sxp: '[:query, [:offset, [:limit, [:scope, [:relation, "objects"], "project"], [:integer, "2"]], [:integer, "3"]]]'
63
+ res: "[%Q{SELECT objects.* FROM objects WHERE objects.project_id = ? LIMIT 2 OFFSET 3}, project_id]"
64
+
65
+ paginate:
66
+ src: "objects in site limit 2 paginate p"
67
+ sxp: '[:query, [:paginate, [:limit, [:scope, [:relation, "objects"], "site"], [:integer, "2"]], [:param, "p"]]]'
68
+ res: "[%Q{SELECT objects.* FROM objects LIMIT 2 OFFSET ?}, ((p.to_i > 0 ? p.to_i : 1)-1)*2]"
69
+
70
+ recipients_or_objects:
71
+ src: recipients or objects
72
+ sxp: '[:query, [:clause_or, [:relation, "recipients"], [:relation, "objects"]]]'
73
+ res: "[%Q{SELECT objects.* FROM links,objects WHERE ((objects.id = links.target_id AND links.relation_id = 4 AND links.source_id = ?) OR objects.parent_id = ?) GROUP BY objects.id}, id, id]"
74
+
75
+ recipients_or_objects_or_letters:
76
+ src: recipients or objects or letters
77
+ res: "[%Q{SELECT objects.* FROM links,objects WHERE ((objects.id = links.target_id AND links.relation_id = 4 AND links.source_id = ?) OR objects.parent_id = ? OR (objects.kpath LIKE 'NNL%' AND objects.parent_id = ?)) GROUP BY objects.id}, id, id, id]"
78
+
79
+ or_clause_with_filter:
80
+ src: "(recipients where name = 'foo') or objects"
81
+ sxp: '[:query, [:clause_or, [:clause_par, [:filter, [:relation, "recipients"], [:"=", [:field, "name"], [:string, "foo"]]]], [:relation, "objects"]]]'
82
+
83
+ count_sql:
84
+ src: "objects in project"
85
+ count: "[%Q{SELECT COUNT(*) FROM objects WHERE objects.project_id = ?}, project_id]"
86
+
87
+ after_process_callback:
88
+ context:
89
+ after_filter: '(1 = 1)'
90
+ src: "objects where name like 'a%' or name like 'b%' in site"
91
+ res: "%Q{SELECT objects.* FROM objects WHERE (1 = 1) AND (objects.name LIKE 'a%' OR objects.name LIKE 'b%')}"