querybuilder 0.5.9 → 0.7.0

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 (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%')}"