fluent-query 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,22 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ gem "hash-utils", ">= 0.18.0"
5
+ gem "abstract", ">= 1.0.0"
6
+ gem "hashie", ">= 1.0.0"
7
+
8
+ # Add dependencies to develop your gem here.
9
+ # Include everything needed to run rake, tests, features, etc.
10
+ group :development do
11
+ gem "bundler", "~> 1.0.13"
12
+ gem "jeweler", "~> 1.6.0"
13
+ end
14
+
15
+
16
+
17
+ # fluent-query-sql
18
+ # fluent-query-dbh
19
+ # fluent-query-mysql
20
+ # fluent-query-sqlite
21
+ # fluent-query-postgresql
22
+ # native-query
data/Gemfile.lock ADDED
@@ -0,0 +1,22 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ abstract (1.0.0)
5
+ git (1.2.5)
6
+ hash-utils (0.18.0)
7
+ hashie (1.0.0)
8
+ jeweler (1.6.3)
9
+ bundler (~> 1.0)
10
+ git (>= 1.2.5)
11
+ rake
12
+ rake (0.9.2)
13
+
14
+ PLATFORMS
15
+ ruby
16
+
17
+ DEPENDENCIES
18
+ abstract (>= 1.0.0)
19
+ bundler (~> 1.0.13)
20
+ hash-utils (>= 0.18.0)
21
+ hashie (>= 1.0.0)
22
+ jeweler (~> 1.6.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 - 2011 Martin Kozák (martinkozak@martinkozak.net)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,230 @@
1
+ Fluent Query
2
+ ============
3
+
4
+ **Fluent Query** is cool way how to write SQL queries and general way
5
+ how to convert series of method calls to string query in an universal
6
+ and system independent manner. It may sounds like a piece of magic, but
7
+ it works. It's inspired by [Dibi][1].
8
+
9
+
10
+ ### General Principle
11
+
12
+ Some example:
13
+
14
+ connection.select("[id], [name]").from("[maintainers]").orderBy("[code] ASC")
15
+
16
+ Will be rendered to:
17
+
18
+ SELECT `id`, `name` FROM `maintainers` ORDER BY `code` ASC
19
+
20
+ It looks trivial, but for example call `connection.heyReturnMeSomething("[yeah]")`
21
+ will be transformed to:
22
+
23
+ HEY RETURN ME SOMETHING `yeah`
24
+
25
+ Which gives big potential. Of sure, escaping, aggregation and chaining
26
+ of chunks for example for `WHERE` directive or another is necessary.
27
+ It's ensured by appropriate *language* (e.g. database) *driver*.
28
+
29
+ And what a more: order of tokens isn't mandatory, so with exception
30
+ of initial world (`SELECT`, `INSERT` etc.) you can add them according to
31
+ your needs.
32
+
33
+ ### Connecting
34
+
35
+ # Include it!
36
+ require "fluent-query/mysql"
37
+ require "fluent-query"
38
+
39
+ # Setup it!
40
+ driver = FluentQuery::Drivers::MySQL
41
+ settings = {
42
+ :username => "wikistatistics.net",
43
+ :password => "alfabeta",
44
+ :server => "localhost",
45
+ :port => 5432,
46
+ :database => "wikistatistics.net",
47
+ :schema => "public"
48
+ }
49
+
50
+ # Create it!
51
+ connection = FluentQuery::Connection::new(driver, settings)
52
+
53
+ Now we have connection prepared for use.
54
+
55
+ ### Placeholders
56
+
57
+ Simple translation calls to queries isn't the only functionality. Very
58
+ helpful are also *placeholders*. They works principially by the same way
59
+ as `#printf` method, but are more suitable for use in queries and
60
+ supports automatic quoting. Available are:
61
+
62
+ * `%%s` which quotes string,
63
+ * `%%i` which quotes integer,
64
+ * `%%b` which quotes boolean,
65
+ * `%%f` which quotes float,
66
+ * `%%d` which quotes date,
67
+ * `%%t` which quotes date-time,
68
+
69
+ And also three special:
70
+
71
+ * `%%sql` which quotes subquery (expects query object),
72
+ * `%%and` which joins input by `AND` operator (expects hash),
73
+ * `%%or` which joins input by `OR` operator (expects hash).
74
+
75
+ An example:
76
+
77
+ connection.select("[id], [name]") \
78
+ .from("[maintainers]") \
79
+ .where("[id] = %%i AND company = %%s", 5, "Wikia") \
80
+ .where("[language] IN %%l", ["cz", "en"]) \
81
+ .or \
82
+ .where("[active] IS %%b", true)
83
+
84
+ Will be transformed to:
85
+
86
+ SELECT `id`, `name` FROM `maintainers`
87
+ WHERE `id` = 5
88
+ AND `company` = "Wikia"
89
+ AND `language` IN ("cz", "en")
90
+ OR `active` IS TRUE
91
+
92
+ It's way how to write complex or special queries. But **direct values
93
+ assigning is supported**, so for example:
94
+
95
+ connection.select(:id, :name) \
96
+ .from(:maintainers) \
97
+ .where(:id => 5, :company => "Wikia") \
98
+ .where("[language] IN %%l", ["cz", "en"]) # %l will join items by commas
99
+ .or \
100
+ .where(:active => true)
101
+
102
+ Will give you expected result too and as you can see, it's much more
103
+ readable, flexible, thus it's preferred.
104
+
105
+ ### Checking Out the Results
106
+
107
+ Query results can be executed by `#execute` which returns result object
108
+ or by `#do` which returns count of affected rows. Following methods for
109
+ checking out the results are available:
110
+
111
+ * `#each` which iterates through all returned rows,
112
+ * `#one` which returns first row only,
113
+ * `#single` which returns first value fo first row,
114
+ * `#assoc` which allows building complex Hashes (see below).
115
+
116
+ #### Associative Fetching
117
+
118
+ Special associative method is the `assoc` one which is directly inspired
119
+ by appropriate feature of the [Dibi][1] layer. It's aim is automatic
120
+ aggregation of returned rows to multidimensional Hashes.
121
+
122
+ Simply give it key names from your dataset. Be warn, only one or two
123
+ levels (e.g. dimesions in resultant Hash) are supported:
124
+
125
+ records = connection.select(:maintainer_id, :language) \
126
+ .from(:sites) \
127
+ .execute.assoc(:maintainer_id, :language)
128
+
129
+ Will transform the dataset:
130
+
131
+ # maintainer_id, language, name
132
+ [1, "en", "English Wikipedia"],
133
+ [1, "es", "Spain Wikipedia"],
134
+ [2, "cs", "Czech Wikihow"],
135
+ [2, "ja", "Japan Wikihow"],
136
+
137
+ To the following structure:
138
+
139
+ 1 => {
140
+ "en" => "English Wikipedia",
141
+ "es" => "Spain Wikipedia"
142
+ },
143
+
144
+ 2 => {
145
+ "cs" => "Czech Wikihow",
146
+ "ja" => "Japan Wikihow"
147
+ }
148
+
149
+ ### Inserts, Updates and Deletes
150
+
151
+ Inserting, updating and deleteing the records works by the same way as
152
+ selecting. Some examples:
153
+
154
+ connection.insert(:maintainers, :name => "Wikimedia", :country => "United States")
155
+
156
+ # Will be:
157
+ # INSERT INTO `maintainers` (`name`, `country`) VALUES ("Wikimedia", "United States")
158
+
159
+ connection.update(:maintainers).set(:country => "Czech Republic").where(:id => 10).limit(1)
160
+
161
+ # Will be:
162
+ # UPDATE `maintainers` SET `country` = "Czech Republic" WHERE `id` = 10 LIMIT 1
163
+
164
+ connection.delete(:maintainers).where(:id => 10).limit(1)
165
+
166
+ # Will be:
167
+ # DELETE FROM `maintainers` WHERE `id` = 10 LIMIT 1
168
+
169
+
170
+ #### Transactions
171
+
172
+ Transactions support is available manual:
173
+
174
+ * `connection.begin`,
175
+ * `connection.commit`,
176
+ * `connection.rollback`.
177
+
178
+ Or by automatic way:
179
+
180
+ connection.transaction do
181
+ #...
182
+ end
183
+
184
+ ### Compiled and Prepared Queries
185
+
186
+ Queries can be pre-prepared and pre-optimized by two different methods:
187
+
188
+ * `#compile` which compiles query to form of array of direct callbacks,
189
+ so builds it and quotes all identifiers, keeps intact placeholders only,
190
+ * `#prepare` which transforms compiled query to prepared form as it's
191
+ known from DBD or PDO if it's supported by driver.
192
+
193
+ Simply call one of these methods upon the query and use resultant query
194
+ as usuall (of sure, without methods which would change it because it's
195
+ compiled so cannot be further changed).
196
+
197
+ Also note, SQL token calls cannot be called by mandatory way (e.g. you
198
+ can call `#order` before `#where` etc.), but they will be reordered
199
+ in resultant both compiled and prepared query, so arguments given to
200
+ execute must be taken to call in correct order according to resultant
201
+ SQL query. So in case of using compiled or prepared statements, it's
202
+ good idea to write calls in the same order as SQL requires.
203
+
204
+ ### Examples
205
+
206
+ More examples or tutorials than these above aren't available. It's in
207
+ development, although stable and production ready state. For well
208
+ documented and fully usable application see [Native Query][4].
209
+
210
+
211
+ Contributing
212
+ ------------
213
+
214
+ 1. Fork it.
215
+ 2. Create a branch (`git checkout -b 20101220-my-change`).
216
+ 3. Commit your changes (`git commit -am "Added something"`).
217
+ 4. Push to the branch (`git push origin 20101220-my-change`).
218
+ 5. Create an [Issue][2] with a link to your branch.
219
+ 6. Enjoy a refreshing Diet Coke and wait.
220
+
221
+ Copyright
222
+ ---------
223
+
224
+ Copyright © 2009-2011 [Martin Kozák][3]. See `LICENSE.txt` for
225
+ further details.
226
+
227
+ [1]: http://dibiphp.com/
228
+ [2]: http://github.com/martinkozak/fluent-query/issues
229
+ [3]: http://www.martinkozak.net/
230
+ [4]: http://github.com/martinkozak/native-query
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ require 'bundler'
4
+
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+
13
+ require 'rake'
14
+ require 'jeweler'
15
+
16
+ Jeweler::Tasks.new do |gem|
17
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
18
+ gem.name = "fluent-query"
19
+ gem.homepage = "http://github.com/martinkozak/fluent-query"
20
+ gem.license = "MIT"
21
+ gem.summary = 'Cool way how to write SQL queries and general way how to convert series of method calls to string query in an universal and system independent manner. This gem contains base libraries only. SQL implementation is available standalone.'
22
+ gem.email = "martinkozak@martinkozak.net"
23
+ gem.authors = ["Martin Kozák"]
24
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
25
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
26
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
27
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
28
+ end
29
+ Jeweler::RubygemsDotOrgTasks.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.9.0
@@ -0,0 +1,75 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{fluent-query}
8
+ s.version = "0.9.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = [%q{Martin Kozák}]
12
+ s.date = %q{2011-07-14}
13
+ s.email = %q{martinkozak@martinkozak.net}
14
+ s.extra_rdoc_files = [
15
+ "LICENSE.txt",
16
+ "README.md"
17
+ ]
18
+ s.files = [
19
+ ".document",
20
+ "Gemfile",
21
+ "Gemfile.lock",
22
+ "LICENSE.txt",
23
+ "README.md",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "fluent-query.gemspec",
27
+ "lib/fluent-query.rb",
28
+ "lib/fluent-query/compiler.rb",
29
+ "lib/fluent-query/compilers/result.rb",
30
+ "lib/fluent-query/connection.rb",
31
+ "lib/fluent-query/data.rb",
32
+ "lib/fluent-query/driver.rb",
33
+ "lib/fluent-query/drivers/exception.rb",
34
+ "lib/fluent-query/drivers/result.rb",
35
+ "lib/fluent-query/exception.rb",
36
+ "lib/fluent-query/queries/abstract.rb",
37
+ "lib/fluent-query/queries/compiled.rb",
38
+ "lib/fluent-query/queries/prepared.rb",
39
+ "lib/fluent-query/queries/processor.rb",
40
+ "lib/fluent-query/query.rb",
41
+ "lib/fluent-query/result.rb",
42
+ "lib/fluent-query/token.rb",
43
+ "lib/fluent-query/tokens/raw.rb"
44
+ ]
45
+ s.homepage = %q{http://github.com/martinkozak/fluent-query}
46
+ s.licenses = [%q{MIT}]
47
+ s.require_paths = [%q{lib}]
48
+ s.rubygems_version = %q{1.8.5}
49
+ s.summary = %q{Cool way how to write SQL queries and general way how to convert series of method calls to string query in an universal and system independent manner. This gem contains base libraries only. SQL implementation is available standalone.}
50
+
51
+ if s.respond_to? :specification_version then
52
+ s.specification_version = 3
53
+
54
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
55
+ s.add_runtime_dependency(%q<hash-utils>, [">= 0.18.0"])
56
+ s.add_runtime_dependency(%q<abstract>, [">= 1.0.0"])
57
+ s.add_runtime_dependency(%q<hashie>, [">= 1.0.0"])
58
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.13"])
59
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.0"])
60
+ else
61
+ s.add_dependency(%q<hash-utils>, [">= 0.18.0"])
62
+ s.add_dependency(%q<abstract>, [">= 1.0.0"])
63
+ s.add_dependency(%q<hashie>, [">= 1.0.0"])
64
+ s.add_dependency(%q<bundler>, ["~> 1.0.13"])
65
+ s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
66
+ end
67
+ else
68
+ s.add_dependency(%q<hash-utils>, [">= 0.18.0"])
69
+ s.add_dependency(%q<abstract>, [">= 1.0.0"])
70
+ s.add_dependency(%q<hashie>, [">= 1.0.0"])
71
+ s.add_dependency(%q<bundler>, ["~> 1.0.13"])
72
+ s.add_dependency(%q<jeweler>, ["~> 1.6.0"])
73
+ end
74
+ end
75
+
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ require "fluent-query/connection"
@@ -0,0 +1,182 @@
1
+ # encoding: utf-8
2
+ require "date"
3
+ require "fluent-query/query"
4
+ require "fluent-query/compilers/result"
5
+ require "hash-utils/array" # >= 0.17.0
6
+
7
+ module FluentQuery
8
+
9
+ ##
10
+ # Query compiler.
11
+ #
12
+
13
+ class Compiler
14
+
15
+ ##
16
+ # Contains all possible formatting directives.
17
+ #
18
+
19
+ FORMATTING_DIRECTIVES = [:i, :s, :l, :b, :f, :d, :t, :sql, :and, :or]
20
+
21
+ ##
22
+ # Indicates directive prefix.
23
+ #
24
+
25
+ DIRECTIVE_PREFIX = "%%"
26
+
27
+ ##
28
+ # Compiling cache.
29
+ #
30
+
31
+ private
32
+ @__compile_cache
33
+
34
+ ##
35
+ # Calls hash cache.
36
+ #
37
+
38
+ private
39
+ @__calls_cache
40
+
41
+ ##
42
+ # Processor upon which compiler should work upon.
43
+ #
44
+
45
+ protected
46
+ @_processor
47
+
48
+ ##
49
+ # Constructor.
50
+ #
51
+
52
+ public
53
+ def initialize(processor)
54
+ @_processor = processor
55
+ end
56
+
57
+ ##
58
+ # Compiles formatting.
59
+ #
60
+
61
+ public
62
+ def compile_formatting(directive)
63
+ self.calls[directive.to_sym]
64
+ end
65
+
66
+ ##
67
+ # Returns calls array.
68
+ #
69
+
70
+ public
71
+ def calls
72
+ if @__calls_cache.nil?
73
+ @__calls_cache = {
74
+ :i => Proc::new { |v| @_processor.quote_value(v.to_i) },
75
+ :s => Proc::new { |v| @_processor.quote_value(v.to_s) },
76
+ :b => Proc::new { |v| @_processor.quote_value(v ? true : false) },
77
+ :f => Proc::new { |v| @_processor.quote_value(v.to_f) },
78
+
79
+ :l => Proc::new do |v|
80
+ if v.array?
81
+ output = v
82
+ elsif v.hash?
83
+ output = v.values
84
+ end
85
+
86
+ "(" << @_processor.process_array(output) << ")"
87
+ end,
88
+
89
+ :d => Proc::new do |v|
90
+ if v.string?
91
+ output = Date.parse(v)
92
+ elsif argument.kind_of? DateTime
93
+ output = Date.parse(v.to_s)
94
+ elsif argument.kind_of? Date
95
+ output = v
96
+ end
97
+
98
+ @_processor.quote_value(output)
99
+ end,
100
+
101
+ :t => Proc::new do |v|
102
+ if v.string?
103
+ output = DateTime.parse(v)
104
+ elsif argument.kind_of? Date
105
+ output = DateTime.parse(v.to_s)
106
+ elsif argument.kind_of? DateTime
107
+ output = v
108
+ end
109
+
110
+ @_processor.quote_value(output)
111
+ end,
112
+
113
+ :sql => Proc::new do |v|
114
+ if v.kind_of? MP::Fluent::Query
115
+ output = v.build!
116
+ end
117
+
118
+ @_processor.quote_subquery(output)
119
+ end,
120
+
121
+ :and => Proc::new do |v|
122
+ operator = @_processor.driver.quote_operator(:and)
123
+ @_processor.process_hash(v, operator)
124
+ end,
125
+
126
+ :or => Proc::new do |v|
127
+ operator = @_processor.driver.quote_operator(:or)
128
+ @_processor.process_hash(v, operator)
129
+ end,
130
+ }
131
+ end
132
+
133
+ @__calls_cache
134
+ end
135
+
136
+ ##
137
+ # Compiles string.
138
+ #
139
+
140
+ public
141
+ def compile(string)
142
+ output = FluentQuery::Compilers::Result::new
143
+ prefix = self.class::DIRECTIVE_PREFIX
144
+ buffer = ""
145
+
146
+ # Builds compile informations
147
+ if not @__compile_cache
148
+ directives = self.class::FORMATTING_DIRECTIVES.map { |s| s.to_s }
149
+ regexp = Regexp::new("^(" << directives.join("|") << ")(?:[^\w]|$)")
150
+
151
+ @__compile_cache = regexp
152
+ else
153
+ regexp = @__compile_cache
154
+ end
155
+
156
+
157
+ # Splits to by directive separated parts
158
+ string.split(prefix).each do |part|
159
+ match = part.match(regexp)
160
+
161
+ if match
162
+ if not buffer.empty?
163
+ output << buffer
164
+ end
165
+
166
+ output << self.compile_formatting(match[1])
167
+ buffer = part[match[1].length..-1]
168
+ else
169
+ buffer << prefix << part
170
+ end
171
+ end
172
+
173
+ output << buffer
174
+
175
+ # Corrects and returns result
176
+ output.first.replace(output.first[prefix.length..-1]) # strips out initial "%%"
177
+ return output
178
+
179
+ end
180
+ end
181
+ end
182
+