sql_wrangler 0.0.1
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/README +1 -0
- data/lib/sql_wrangler.rb +149 -0
- data/test/sql_wrangler_tests.rb +168 -0
- metadata +58 -0
data/README
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Makes it easier to deal with SQL queries and handle their results.
|
data/lib/sql_wrangler.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
|
3
|
+
module SqlWrangler
|
4
|
+
|
5
|
+
class SqlConnection
|
6
|
+
|
7
|
+
def query(sql_string)
|
8
|
+
Query.new self, sql_string
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class SqLiteConnection < SqlConnection
|
14
|
+
|
15
|
+
def initialize(db_path)
|
16
|
+
@db = SQLite3::Database.new db_path
|
17
|
+
end
|
18
|
+
|
19
|
+
def execute_sql(sql_string)
|
20
|
+
@db.execute2(sql_string)
|
21
|
+
end
|
22
|
+
|
23
|
+
def command(sql_string)
|
24
|
+
@db.execute(sql_string)
|
25
|
+
end
|
26
|
+
|
27
|
+
def close
|
28
|
+
@db.close
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
class Query
|
34
|
+
|
35
|
+
attr_reader :sql_string
|
36
|
+
attr_reader :groupings
|
37
|
+
attr_reader :conn
|
38
|
+
|
39
|
+
def initialize(conn, sql_string)
|
40
|
+
@conn = conn
|
41
|
+
@sql_string = sql_string
|
42
|
+
@groupings = []
|
43
|
+
end
|
44
|
+
|
45
|
+
def execute
|
46
|
+
raw_result = @conn.execute_sql(@sql_string)
|
47
|
+
init_groups_for_execution raw_result[0]
|
48
|
+
return format_query_result(raw_result)
|
49
|
+
end
|
50
|
+
|
51
|
+
def format_query_result(raw_result)
|
52
|
+
formatted_result = []
|
53
|
+
columns_by_index = get_columns_by_index raw_result[0]
|
54
|
+
|
55
|
+
raw_result[1,raw_result.length-1].each do |raw_row|
|
56
|
+
merge_row raw_row, @groupings, formatted_result, columns_by_index
|
57
|
+
end
|
58
|
+
|
59
|
+
return formatted_result
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_columns_by_index columns
|
63
|
+
columns_by_index = {}
|
64
|
+
(0..columns.length-1).each { |i| columns_by_index[i] = columns[i] }
|
65
|
+
return columns_by_index
|
66
|
+
end
|
67
|
+
|
68
|
+
def init_groups_for_execution columns
|
69
|
+
used_indexes = []
|
70
|
+
@groupings.each do |group|
|
71
|
+
group.group_indexes = []
|
72
|
+
group.content_indexes = []
|
73
|
+
(0..(columns.length-1)).each do |i|
|
74
|
+
if group.columns.any? { |c| c == columns[i] }
|
75
|
+
used_indexes << i
|
76
|
+
group.group_indexes << i
|
77
|
+
elsif not used_indexes.any? { |used_index| used_index == i }
|
78
|
+
group.content_indexes << i
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def merge_row(row, groups, grouped_data, columns_by_index)
|
85
|
+
|
86
|
+
if not @groupings.any?
|
87
|
+
flat_row = {}
|
88
|
+
columns_by_index.each { |index, column| flat_row[column] = row[index] }
|
89
|
+
grouped_data << flat_row
|
90
|
+
elsif groups.any?
|
91
|
+
this_group = groups[0]
|
92
|
+
remaining_groups = groups[1, groups.length-1]
|
93
|
+
grouped_vals = {}
|
94
|
+
this_group.group_indexes.each { |index| grouped_vals[columns_by_index[index]] = row[index] }
|
95
|
+
|
96
|
+
if grouped_vals.values.any? { |value| value != nil }
|
97
|
+
grouped_row = get_existing_grouped_row grouped_vals, grouped_data
|
98
|
+
|
99
|
+
if grouped_row == nil
|
100
|
+
grouped_row = grouped_vals
|
101
|
+
grouped_row[this_group.name] = [] if this_group != nil
|
102
|
+
grouped_data << grouped_row
|
103
|
+
end
|
104
|
+
|
105
|
+
merge_row(row, remaining_groups, grouped_row[this_group.name], columns_by_index)
|
106
|
+
end
|
107
|
+
|
108
|
+
else
|
109
|
+
leaf_data = {}
|
110
|
+
@groupings.last.content_indexes.each { |i| leaf_data[columns_by_index[i]] = row[i] }
|
111
|
+
grouped_data << leaf_data if leaf_data.values.any? { |value| value != nil }
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_existing_grouped_row grouped_vals, grouped_data
|
117
|
+
grouped_data.each do |grouped_row|
|
118
|
+
if not grouped_vals.keys.any? { |key| grouped_vals[key] != grouped_row[key] }
|
119
|
+
return grouped_row
|
120
|
+
end
|
121
|
+
end
|
122
|
+
return nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def group(name, columns)
|
126
|
+
new_grouping = QueryGrouping.new(name, columns)
|
127
|
+
new_grouping.level = @groupings.length
|
128
|
+
@groupings << new_grouping
|
129
|
+
return self
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
class QueryGrouping
|
135
|
+
|
136
|
+
attr_reader :name
|
137
|
+
attr_reader :columns
|
138
|
+
attr_accessor :content_indexes
|
139
|
+
attr_accessor :group_indexes
|
140
|
+
attr_accessor :level
|
141
|
+
|
142
|
+
def initialize(name, columns)
|
143
|
+
@name = name
|
144
|
+
@columns = columns
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'fas_test'
|
2
|
+
require './lib/sql_wrangler'
|
3
|
+
|
4
|
+
class SqLiteConnectionTests < FasTest::TestClass
|
5
|
+
|
6
|
+
def class_setup
|
7
|
+
@conn = SqlWrangler::SqLiteConnection.new ":memory:"
|
8
|
+
end
|
9
|
+
|
10
|
+
def class_teardown
|
11
|
+
@conn.close
|
12
|
+
end
|
13
|
+
|
14
|
+
def test__command
|
15
|
+
begin
|
16
|
+
@conn.command("CREATE TABLE users (id int PRIMARY KEY, username VARCHAR(100), password VARCHAR(100))")
|
17
|
+
@conn.command("INSERT INTO users VALUES (1, 'username1', 'password1')")
|
18
|
+
result = @conn.execute_sql("SELECT * FROM users")
|
19
|
+
assert_equal(2, result.length)
|
20
|
+
ensure
|
21
|
+
@conn.command("DROP TABLE users")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def test__query__without_executing_it
|
26
|
+
query = @conn.query("SELECT * FROM users")
|
27
|
+
assert_equal("SELECT * FROM users", query.sql_string)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
class QueryTests < FasTest::TestClass
|
33
|
+
|
34
|
+
def class_setup
|
35
|
+
@conn = SqlWrangler::SqLiteConnection.new ":memory:"
|
36
|
+
@conn.execute_sql("CREATE TABLE users (username VARCHAR(100), password VARCHAR(100))")
|
37
|
+
@conn.execute_sql("CREATE TABLE groups (group_name VARCHAR(100))")
|
38
|
+
@conn.execute_sql("CREATE TABLE users_groups (username VARCHAR(100), group_name VARCHAR(100))")
|
39
|
+
|
40
|
+
@conn.execute_sql("create table articles (id int primary key, title varchar(100), content varchar(100), author varchar(100))")
|
41
|
+
@conn.execute_sql("create table comments (id int primary key, content text, article_id int)")
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_setup
|
45
|
+
@conn.execute_sql("INSERT INTO users VALUES ('username1', 'password1');")
|
46
|
+
@conn.execute_sql("INSERT INTO users VALUES ('username2', 'password2');")
|
47
|
+
@conn.execute_sql("INSERT INTO groups VALUES ('group one')")
|
48
|
+
@conn.execute_sql("INSERT INTO groups VALUES ('group two')")
|
49
|
+
@conn.execute_sql("INSERT INTO users_groups VALUES ('username1', 'group one')")
|
50
|
+
@conn.execute_sql("INSERT INTO users_groups VALUES ('username1', 'group two')")
|
51
|
+
@conn.execute_sql("INSERT INTO users_groups VALUES ('username2', 'group one')")
|
52
|
+
|
53
|
+
@conn.execute_sql("insert into articles values (1, 'article1', 'content1', 'username1')")
|
54
|
+
@conn.execute_sql("insert into comments values (1, 'comment on article1 #1', 1)")
|
55
|
+
@conn.execute_sql("insert into comments values (2, 'comment on article1 #2', 1)")
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_teardown
|
59
|
+
@conn.execute_sql("delete from users")
|
60
|
+
@conn.execute_sql("delete from groups")
|
61
|
+
@conn.execute_sql("delete from users_groups")
|
62
|
+
@conn.execute_sql("delete from articles")
|
63
|
+
@conn.execute_sql("delete from comments")
|
64
|
+
end
|
65
|
+
|
66
|
+
def class_teardown
|
67
|
+
@conn.close
|
68
|
+
end
|
69
|
+
|
70
|
+
def test__execute__has_correct_values_on_simple_query
|
71
|
+
result = @conn.query("SELECT * FROM users").execute
|
72
|
+
assert_equal(2, result.length)
|
73
|
+
assert_equal("username1", result[0]['username'])
|
74
|
+
assert_equal("password1", result[0]['password'])
|
75
|
+
assert_equal("username2", result[1]['username'])
|
76
|
+
assert_equal("password2", result[1]['password'])
|
77
|
+
end
|
78
|
+
|
79
|
+
def test__execute__has_correct_columns_on_simple_query
|
80
|
+
first = @conn.query("SELECT * FROM users").execute[0]
|
81
|
+
assert_true(first.keys.any? { |m| m == 'username' })
|
82
|
+
assert_true(first.keys.any? { |m| m == 'password' })
|
83
|
+
end
|
84
|
+
|
85
|
+
def test__execute__works_with_a_more_complex_query
|
86
|
+
result = @conn.query("
|
87
|
+
select u.username, g.group_name
|
88
|
+
from groups g
|
89
|
+
inner join users_groups ug on ug.group_name = g.group_name
|
90
|
+
inner join users u on u.username = ug.username
|
91
|
+
order by u.username, g.group_name").execute
|
92
|
+
assert_equal(3, result.length)
|
93
|
+
assert_equal("username1", result[0]["username"])
|
94
|
+
assert_equal("group one", result[0]["group_name"])
|
95
|
+
assert_equal("username1", result[1]["username"])
|
96
|
+
assert_equal("group two", result[1]["group_name"])
|
97
|
+
assert_equal("username2", result[2]["username"])
|
98
|
+
assert_equal("group one", result[2]["group_name"])
|
99
|
+
end
|
100
|
+
|
101
|
+
def test__execute__works_with_a_simple_grouping
|
102
|
+
result = @conn.query("
|
103
|
+
select u.username, g.group_name
|
104
|
+
from groups g
|
105
|
+
inner join users_groups ug on ug.group_name = g.group_name
|
106
|
+
inner join users u on u.username = ug.username
|
107
|
+
order by u.username, g.group_name").group("users", ["group_name"]).execute
|
108
|
+
assert_equal(2, result.length)
|
109
|
+
assert_equal("group one", result[0]["group_name"])
|
110
|
+
assert_equal(2, result[0]["users"].length)
|
111
|
+
assert_equal("username1", result[0]["users"][0]["username"])
|
112
|
+
assert_equal("username2", result[0]["users"][1]["username"])
|
113
|
+
assert_equal("group two", result[1]["group_name"])
|
114
|
+
assert_equal(1, result[1]["users"].length)
|
115
|
+
assert_equal("username1", result[1]["users"][0]["username"])
|
116
|
+
end
|
117
|
+
|
118
|
+
def test__group__modifies_query_object_correctly_with_single_grouping
|
119
|
+
query = @conn.query("
|
120
|
+
select u.username, g.group_name
|
121
|
+
from groups g
|
122
|
+
inner join users_groups ug on ug.group_name = g.group_name
|
123
|
+
inner join users u on u.username = ug.username
|
124
|
+
order by u.username, g.group_name").group("users", ["group_name"])
|
125
|
+
assert_equal(1, query.groupings.count)
|
126
|
+
assert_equal("users", query.groupings[0].name)
|
127
|
+
assert_equal(1, query.groupings[0].columns.length)
|
128
|
+
assert_equal("group_name", query.groupings[0].columns[0])
|
129
|
+
end
|
130
|
+
|
131
|
+
def test__execute__works_with_multi_level_grouping
|
132
|
+
result = @conn.query("
|
133
|
+
select u.username, g.group_name, a.title, a.content article_content, c.id comment_id, c.content comment_content
|
134
|
+
from groups g
|
135
|
+
join users_groups ug on ug.group_name = g.group_name
|
136
|
+
join users u on u.username = ug.username
|
137
|
+
left join articles a on a.author = u.username
|
138
|
+
left join comments c on c.article_id = a.id
|
139
|
+
order by g.group_name, u.username, a.title, c.content") \
|
140
|
+
.group("users", ["group_name"]) \
|
141
|
+
.group("articles", ["username"]) \
|
142
|
+
.group("comments", ["article_content", "title"]) \
|
143
|
+
.execute
|
144
|
+
|
145
|
+
assert_equal(2, result.length)
|
146
|
+
assert_equal("group one", result[0]["group_name"])
|
147
|
+
assert_equal(2, result[0]["users"].length)
|
148
|
+
assert_equal("username1", result[0]["users"][0]["username"])
|
149
|
+
|
150
|
+
assert_equal(1, result[0]["users"][0]["articles"].length)
|
151
|
+
assert_equal("article1", result[0]["users"][0]["articles"][0]["title"])
|
152
|
+
assert_equal("content1", result[0]["users"][0]["articles"][0]["article_content"])
|
153
|
+
|
154
|
+
assert_equal(2, result[0]["users"][0]["articles"][0]["comments"].length)
|
155
|
+
assert_equal(1, result[0]["users"][0]["articles"][0]["comments"][0]["comment_id"])
|
156
|
+
assert_equal("comment on article1 #1", result[0]["users"][0]["articles"][0]["comments"][0]["comment_content"])
|
157
|
+
assert_equal(2, result[0]["users"][0]["articles"][0]["comments"][1]["comment_id"])
|
158
|
+
assert_equal("comment on article1 #2", result[0]["users"][0]["articles"][0]["comments"][1]["comment_content"])
|
159
|
+
assert_equal("username2", result[0]["users"][1]["username"])
|
160
|
+
assert_equal(0, result[0]["users"][1]["articles"].length)
|
161
|
+
|
162
|
+
assert_equal("group two", result[1]["group_name"])
|
163
|
+
assert_equal(1, result[1]["users"].length)
|
164
|
+
assert_equal("username1", result[1]["users"][0]["username"])
|
165
|
+
assert_equal(1, result[1]["users"][0]["articles"].length)
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sql_wrangler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Graeme Hill
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-02-28 00:00:00 -08:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: Makes it easier to deal with SQL queries and handle their results.
|
18
|
+
email: graemekh@gmail.com
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files:
|
24
|
+
- README
|
25
|
+
files:
|
26
|
+
- README
|
27
|
+
- lib/sql_wrangler.rb
|
28
|
+
- test/sql_wrangler_tests.rb
|
29
|
+
has_rdoc: true
|
30
|
+
homepage: https://github.com/graeme-hill/sql_wrangler
|
31
|
+
licenses: []
|
32
|
+
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options: []
|
35
|
+
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: "0"
|
50
|
+
requirements: []
|
51
|
+
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 1.5.2
|
54
|
+
signing_key:
|
55
|
+
specification_version: 3
|
56
|
+
summary: A simple ORM alternative that makes it easier to deal with SQL queries in ruby.
|
57
|
+
test_files:
|
58
|
+
- test/sql_wrangler_tests.rb
|