mao 0.0.3
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/.autotest +2 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +18 -0
- data/README.md +14 -0
- data/Rakefile +8 -0
- data/TODO +5 -0
- data/lib/mao/filter.rb +168 -0
- data/lib/mao/query.rb +381 -0
- data/lib/mao/version.rb +5 -0
- data/lib/mao.rb +180 -0
- data/mao.gemspec +22 -0
- data/spec/filter_spec.rb +173 -0
- data/spec/fixture.sql +38 -0
- data/spec/mao_spec.rb +220 -0
- data/spec/query_spec.rb +417 -0
- data/spec/spec_helper.rb +13 -0
- data/thoughts.rb +42 -0
- metadata +120 -0
data/lib/mao.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'pg'
|
2
|
+
require 'bigdecimal'
|
3
|
+
|
4
|
+
# The top-level module to access Mao.
|
5
|
+
module Mao
|
6
|
+
require 'mao/query'
|
7
|
+
|
8
|
+
# Connect to the database. +options+ is currently the straight Postgres gem
|
9
|
+
# options.
|
10
|
+
def self.connect!(options)
|
11
|
+
unless @conn
|
12
|
+
@conn = PG.connect(options)
|
13
|
+
@conn.internal_encoding = Encoding::UTF_8
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Disconnect from the database.
|
18
|
+
def self.disconnect!
|
19
|
+
@conn.close
|
20
|
+
@conn = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
# Execute the raw SQL +sql+ with positional +args+. The returned object
|
24
|
+
# varies depending on the database vendor.
|
25
|
+
def self.sql(sql, *args, &block)
|
26
|
+
STDERR.puts "#{sql}#{args ? " " + args.inspect : ""}"
|
27
|
+
@conn.exec(sql, *args, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Quote +name+ as appropriate for a table or column name in an SQL statement.
|
31
|
+
def self.quote_ident(name)
|
32
|
+
case name
|
33
|
+
when Symbol
|
34
|
+
@conn.quote_ident(name.to_s)
|
35
|
+
else
|
36
|
+
@conn.quote_ident(name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Escape +value+ as appropriate for a literal in an SQL statement.
|
41
|
+
def self.escape_literal(value)
|
42
|
+
case value
|
43
|
+
when String
|
44
|
+
@conn.escape_literal(value)
|
45
|
+
when NilClass
|
46
|
+
"null"
|
47
|
+
when TrueClass
|
48
|
+
"true"
|
49
|
+
when FalseClass
|
50
|
+
"false"
|
51
|
+
when Numeric
|
52
|
+
value.to_s
|
53
|
+
when Array
|
54
|
+
if value == []
|
55
|
+
# NULL IN NULL is NULL in SQL, so this is "safe". Empty lists () are
|
56
|
+
# apparently part of the standard, but not widely supported (!).
|
57
|
+
"(null)"
|
58
|
+
else
|
59
|
+
"(#{value.map {|v| escape_literal(v)}.join(", ")})"
|
60
|
+
end
|
61
|
+
when Mao::Query::Raw
|
62
|
+
value.text
|
63
|
+
when Time
|
64
|
+
escape_literal(value.utc.strftime("%Y-%m-%d %H:%M:%S.%6N"))
|
65
|
+
else
|
66
|
+
raise ArgumentError, "don't know how to escape #{value.class}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns a new Mao::Query object for +table+.
|
71
|
+
def self.query(table)
|
72
|
+
@queries ||= {}
|
73
|
+
@queries[table] ||= Query.new(table).freeze
|
74
|
+
end
|
75
|
+
|
76
|
+
# When raised in a transaction, causes a rollback without the exception
|
77
|
+
# bubbling.
|
78
|
+
Rollback = Class.new(Exception)
|
79
|
+
|
80
|
+
# Executes +block+ in a transaction.
|
81
|
+
#
|
82
|
+
# If +block+ executes without an exception, the transaction is committed.
|
83
|
+
#
|
84
|
+
# If a Mao::Rollback is raised, the transaction is rolled back, and
|
85
|
+
# #transaction returns false.
|
86
|
+
#
|
87
|
+
# If any other Exception is raised, the transaction is rolled back, and the
|
88
|
+
# exception is re-raised.
|
89
|
+
#
|
90
|
+
# Otherwise, the transaction is committed, and the result of +block+ is
|
91
|
+
# returned.
|
92
|
+
def self.transaction(&block)
|
93
|
+
return block.call if @in_transaction
|
94
|
+
@in_transaction = true
|
95
|
+
|
96
|
+
sql("BEGIN")
|
97
|
+
begin
|
98
|
+
r = block.call
|
99
|
+
rescue Rollback
|
100
|
+
sql("ROLLBACK")
|
101
|
+
return false
|
102
|
+
rescue Exception
|
103
|
+
sql("ROLLBACK")
|
104
|
+
raise
|
105
|
+
ensure
|
106
|
+
@in_transaction = false
|
107
|
+
end
|
108
|
+
sql("COMMIT")
|
109
|
+
r
|
110
|
+
end
|
111
|
+
|
112
|
+
# Normalizes the Hash +result+ (of Strings to Strings), with +col_types+
|
113
|
+
# specifying Symbol column names to String PostgreSQL types.
|
114
|
+
def self.normalize_result(result, col_types)
|
115
|
+
Hash[result.map {|k,v|
|
116
|
+
k = k.to_sym
|
117
|
+
[k, convert_type(v, col_types[k])]
|
118
|
+
}]
|
119
|
+
end
|
120
|
+
|
121
|
+
# Normalizes the Hash +result+ (of Strings to Strings), with the joining
|
122
|
+
# tables of +from_query+ and +to_query+. Assumes the naming convention for
|
123
|
+
# result keys of Mao::Query#join (see Mao::Query#sql) has been followed.
|
124
|
+
def self.normalize_join_result(result, from_query, to_query)
|
125
|
+
results = {}
|
126
|
+
n = 0
|
127
|
+
|
128
|
+
from_table = from_query.table
|
129
|
+
from_types = from_query.col_types
|
130
|
+
from_types.keys.sort.each do |k|
|
131
|
+
n += 1
|
132
|
+
key = "c#{n}"
|
133
|
+
if result.include?(key)
|
134
|
+
results[from_table] ||= {}
|
135
|
+
results[from_table][k] = convert_type(result[key], from_types[k])
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
to_table = to_query.table
|
140
|
+
to_types = to_query.col_types
|
141
|
+
to_types.keys.sort.each do |k|
|
142
|
+
n += 1
|
143
|
+
key = "c#{n}"
|
144
|
+
if result.include?(key)
|
145
|
+
results[to_table] ||= {}
|
146
|
+
results[to_table][k] = convert_type(result[key], to_types[k])
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
results
|
151
|
+
end
|
152
|
+
|
153
|
+
# Converts +value+ to a native Ruby value, based on the PostgreSQL type
|
154
|
+
# +type+.
|
155
|
+
def self.convert_type(value, type)
|
156
|
+
return nil if value.nil?
|
157
|
+
|
158
|
+
case type
|
159
|
+
when "integer", "smallint", "bigint", "serial", "bigserial"
|
160
|
+
value.to_i
|
161
|
+
when /^character varying/, "text"
|
162
|
+
value
|
163
|
+
when "timestamp without time zone"
|
164
|
+
# We assume it's in UTC. (Dangerous?)
|
165
|
+
value =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)$/
|
166
|
+
Time.new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_f, 0)
|
167
|
+
when "boolean"
|
168
|
+
value == "t"
|
169
|
+
when "bytea"
|
170
|
+
PG::Connection.unescape_bytea(value)
|
171
|
+
when "numeric"
|
172
|
+
BigDecimal.new(value)
|
173
|
+
else
|
174
|
+
STDERR.puts "#{self.name}: unknown type: #{type}"
|
175
|
+
value
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# vim: set sw=2 cc=80 et:
|
data/mao.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path('../lib/mao/version', __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Timothy Leslie Allen", "Arlen Christian Mart Cuss"]
|
5
|
+
gem.email = ["allen.timothy.email@gmail.com", "ar@len.me"]
|
6
|
+
gem.description = %q{Mao Ain't an ORM}
|
7
|
+
gem.summary = %q{A database access layer. Currently supports PG.}
|
8
|
+
gem.homepage = "https://github.com/unnali/mao" # TBD
|
9
|
+
|
10
|
+
gem.add_dependency('pg')
|
11
|
+
gem.add_development_dependency('rake')
|
12
|
+
gem.add_development_dependency('rspec')
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split($\)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.name = "mao"
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.version = Mao::VERSION
|
20
|
+
end
|
21
|
+
|
22
|
+
# vim: set sw=2 et cc=80:
|
data/spec/filter_spec.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mao::Filter do
|
4
|
+
before { prepare_spec }
|
5
|
+
|
6
|
+
let(:col_x) { Mao::Filter::Column.new(:name => :x) }
|
7
|
+
let(:col_y) { Mao::Filter::Column.new(:name => :y) }
|
8
|
+
|
9
|
+
describe ".finalize_or_literal" do
|
10
|
+
context "with Mao::Filter" do
|
11
|
+
before { col_x.should_receive(:finalize).
|
12
|
+
with(no_args).and_return("blah") }
|
13
|
+
it { Mao::Filter.finalize_or_literal(col_x).should eq "blah" }
|
14
|
+
end
|
15
|
+
|
16
|
+
context "with non-Mao::Filter" do
|
17
|
+
before { Mao.should_receive(:escape_literal).
|
18
|
+
with(42).and_return("ha") }
|
19
|
+
it { Mao::Filter.finalize_or_literal(42).should eq "ha" }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe ".sql" do
|
24
|
+
context "Arrays" do
|
25
|
+
let(:klass) { double("klass") }
|
26
|
+
before { Mao::Filter.should_receive(:const_get).
|
27
|
+
with(:Hullo).and_return(klass) }
|
28
|
+
before { klass.should_receive(:sql).with(:mao).and_return :dengxiaoping }
|
29
|
+
it { Mao::Filter.sql([:Hullo, :mao]).should eq :dengxiaoping }
|
30
|
+
end
|
31
|
+
|
32
|
+
context "Strings" do
|
33
|
+
it { Mao::Filter.sql("BATTLE ROYALE").should eq "BATTLE ROYALE" }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#and" do
|
38
|
+
subject { col_x.and(col_y) }
|
39
|
+
|
40
|
+
it { should be_an_instance_of Mao::Filter::Binary }
|
41
|
+
it { subject.options[:op].should eq "AND" }
|
42
|
+
it { subject.options[:lhs].should be col_x }
|
43
|
+
it { subject.options[:rhs].should be col_y }
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#or" do
|
47
|
+
subject { col_x.or(col_y) }
|
48
|
+
|
49
|
+
it { should be_an_instance_of Mao::Filter::Binary }
|
50
|
+
it { subject.options[:op].should eq "OR" }
|
51
|
+
it { subject.options[:lhs].should be col_x }
|
52
|
+
it { subject.options[:rhs].should be col_y }
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#==" do
|
56
|
+
subject { col_x == col_y }
|
57
|
+
|
58
|
+
it { should be_an_instance_of Mao::Filter::Binary }
|
59
|
+
it { subject.options[:op].should eq "=" }
|
60
|
+
it { subject.options[:lhs].should be col_x }
|
61
|
+
it { subject.options[:rhs].should be col_y }
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#!=" do
|
65
|
+
subject { col_x != col_y }
|
66
|
+
|
67
|
+
it { should be_an_instance_of Mao::Filter::Binary }
|
68
|
+
it { subject.options[:op].should eq "<>" }
|
69
|
+
it { subject.options[:lhs].should be col_x }
|
70
|
+
it { subject.options[:rhs].should be col_y }
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#>" do
|
74
|
+
subject { col_x > col_y }
|
75
|
+
|
76
|
+
it { should be_an_instance_of Mao::Filter::Binary }
|
77
|
+
it { subject.options[:op].should eq ">" }
|
78
|
+
it { subject.options[:lhs].should be col_x }
|
79
|
+
it { subject.options[:rhs].should be col_y }
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#>=" do
|
83
|
+
subject { col_x >= col_y }
|
84
|
+
|
85
|
+
it { should be_an_instance_of Mao::Filter::Binary }
|
86
|
+
it { subject.options[:op].should eq ">=" }
|
87
|
+
it { subject.options[:lhs].should be col_x }
|
88
|
+
it { subject.options[:rhs].should be col_y }
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#<" do
|
92
|
+
subject { col_x < col_y }
|
93
|
+
|
94
|
+
it { should be_an_instance_of Mao::Filter::Binary }
|
95
|
+
it { subject.options[:op].should eq "<" }
|
96
|
+
it { subject.options[:lhs].should be col_x }
|
97
|
+
it { subject.options[:rhs].should be col_y }
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "#<=" do
|
101
|
+
subject { col_x <= col_y }
|
102
|
+
|
103
|
+
it { should be_an_instance_of Mao::Filter::Binary }
|
104
|
+
it { subject.options[:op].should eq "<=" }
|
105
|
+
it { subject.options[:lhs].should be col_x }
|
106
|
+
it { subject.options[:rhs].should be col_y }
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "#null?" do
|
110
|
+
subject { col_x.null? }
|
111
|
+
|
112
|
+
it { should be_an_instance_of Mao::Filter::Binary }
|
113
|
+
it { subject.options[:op].should eq "IS" }
|
114
|
+
it { subject.options[:lhs].should be col_x }
|
115
|
+
it { subject.options[:rhs].should be_nil }
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "#in" do
|
119
|
+
subject { col_x.in([1, 2, 3]) }
|
120
|
+
|
121
|
+
it { should be_an_instance_of Mao::Filter::Binary }
|
122
|
+
it { subject.options[:op].should eq "IN" }
|
123
|
+
it { subject.options[:lhs].should be col_x }
|
124
|
+
it { subject.options[:rhs].should eq [1, 2, 3] }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe Mao::Filter::Column do
|
129
|
+
before { prepare_spec }
|
130
|
+
|
131
|
+
context "without table" do
|
132
|
+
subject { Mao::Filter::Column.new(:name => :Margorth) }
|
133
|
+
its(:finalize) { should eq [:Column, :Margorth] }
|
134
|
+
it { Mao::Filter.sql(subject.finalize).should eq '"Margorth"' }
|
135
|
+
end
|
136
|
+
|
137
|
+
context "with table" do
|
138
|
+
subject { Mao::Filter::Column.new(:table => :Lol, :name => :Margorth) }
|
139
|
+
its(:finalize) { should eq [:Column, :Lol, :Margorth] }
|
140
|
+
it { Mao::Filter.sql(subject.finalize).should eq '"Lol"."Margorth"' }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe Mao::Filter::Binary do
|
145
|
+
before { prepare_spec }
|
146
|
+
subject { Mao::Filter::Binary.new(:lhs => 42, :op => '=', :rhs => 42) }
|
147
|
+
|
148
|
+
its(:finalize) { should eq [:Binary, '=', "42", "42"] }
|
149
|
+
it { Mao::Filter.sql(subject.finalize).should eq "(42 = 42)" }
|
150
|
+
end
|
151
|
+
|
152
|
+
describe Mao::Filter::Table do
|
153
|
+
before { prepare_spec }
|
154
|
+
|
155
|
+
context "non-explicit" do
|
156
|
+
let(:some) { Mao::Filter::Table.new(Mao.query(:some), false) }
|
157
|
+
it { some.value.should be_an_instance_of Mao::Filter::Column }
|
158
|
+
it { some.value.finalize.should eq [:Column, :value] }
|
159
|
+
end
|
160
|
+
|
161
|
+
context "explicit" do
|
162
|
+
let(:some) { Mao::Filter::Table.new(Mao.query(:some), true) }
|
163
|
+
it { some.value.should be_an_instance_of Mao::Filter::Column }
|
164
|
+
it { some.value.finalize.should eq [:Column, :some, :value] }
|
165
|
+
end
|
166
|
+
|
167
|
+
context "non-extant" do
|
168
|
+
let(:some) { Mao::Filter::Table.new(Mao.query(:some), false) }
|
169
|
+
it { expect { some.blargh }.to raise_exception(ArgumentError) }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# vim: set sw=2 cc=80 et:
|
data/spec/fixture.sql
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
DROP TABLE IF EXISTS empty;
|
2
|
+
CREATE TABLE empty (
|
3
|
+
id INT,
|
4
|
+
value VARCHAR(200));
|
5
|
+
|
6
|
+
DROP TABLE IF EXISTS one;
|
7
|
+
CREATE TABLE one (
|
8
|
+
id INT,
|
9
|
+
value VARCHAR(200));
|
10
|
+
|
11
|
+
INSERT INTO one (id, value) VALUES (42, '你好, Dave.');
|
12
|
+
|
13
|
+
DROP TABLE IF EXISTS "some";
|
14
|
+
CREATE TABLE "some" (
|
15
|
+
id INT,
|
16
|
+
value VARCHAR(100));
|
17
|
+
|
18
|
+
INSERT INTO "some" (id, value) VALUES (1, 'Bah'), (2, 'Hah'), (3, '你好, Dave.');
|
19
|
+
|
20
|
+
DROP TABLE IF EXISTS typey;
|
21
|
+
CREATE TABLE typey (
|
22
|
+
korea BOOLEAN DEFAULT TRUE,
|
23
|
+
japan DECIMAL(19,3),
|
24
|
+
china BYTEA);
|
25
|
+
|
26
|
+
INSERT INTO typey (korea, japan, china) VALUES (true, 1234567890123456.789, E'WHAT\\000'), (false, -1234567890123456.789, E'HUH\\001\\002');
|
27
|
+
|
28
|
+
DROP TABLE IF EXISTS autoid;
|
29
|
+
CREATE TABLE autoid (
|
30
|
+
id SERIAL PRIMARY KEY NOT NULL,
|
31
|
+
value VARCHAR(100));
|
32
|
+
|
33
|
+
DROP TABLE IF EXISTS times;
|
34
|
+
CREATE TABLE times (
|
35
|
+
id SERIAL PRIMARY KEY NOT NULL,
|
36
|
+
time TIMESTAMP WITHOUT TIME ZONE NOT NULL);
|
37
|
+
|
38
|
+
INSERT INTO times (time) VALUES ('2012-11-10T19:45:00Z');
|
data/spec/mao_spec.rb
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe Mao do
|
5
|
+
before { prepare_spec }
|
6
|
+
|
7
|
+
describe ".connect!" do
|
8
|
+
let(:options) { double("options") }
|
9
|
+
let(:conn) { double("conn") }
|
10
|
+
before { PG.should_receive(:connect).with(options).and_return(conn) }
|
11
|
+
before { conn.should_receive(:internal_encoding=).with(Encoding::UTF_8) }
|
12
|
+
before { Mao.disconnect! rescue false }
|
13
|
+
it { Mao.connect!(options) }
|
14
|
+
after { Mao.instance_variable_set("@conn", nil) }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe ".disconnect!" do
|
18
|
+
before { PG::Connection.any_instance.should_receive(:close) }
|
19
|
+
it { Mao.disconnect! }
|
20
|
+
end
|
21
|
+
|
22
|
+
describe ".sql" do
|
23
|
+
before { PG::Connection.any_instance.should_receive(:exec).
|
24
|
+
with(:x).and_return(:y) }
|
25
|
+
it { Mao.sql(:x).should eq :y }
|
26
|
+
end
|
27
|
+
|
28
|
+
describe ".quote_ident" do
|
29
|
+
context "pass-thru" do
|
30
|
+
before { PG::Connection.any_instance.should_receive(:quote_ident).
|
31
|
+
with("table").and_return(%q{"table"}) }
|
32
|
+
it { Mao.quote_ident("table").should eq %q{"table"} }
|
33
|
+
end
|
34
|
+
|
35
|
+
context "Symbols" do
|
36
|
+
before { PG::Connection.any_instance.should_receive(:quote_ident).
|
37
|
+
with("table").and_return(%q{"table"}) }
|
38
|
+
it { Mao.quote_ident(:table).should eq %q{"table"} }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe ".escape_literal" do
|
43
|
+
describe "verify pass-thru String" do
|
44
|
+
before { PG::Connection.any_instance.should_receive(:escape_literal).
|
45
|
+
with("table").and_return(%q{'table'}) }
|
46
|
+
it { Mao.escape_literal("table").should eq %q{'table'} }
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "verify not pass-thru others" do
|
50
|
+
before { PG::Connection.any_instance.
|
51
|
+
should_not_receive(:escape_literal) }
|
52
|
+
it { Mao.escape_literal(nil).should eq "null" }
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "actual values" do
|
56
|
+
it { Mao.escape_literal("table").should eq %q{'table'} }
|
57
|
+
it { Mao.escape_literal(42).should eq %q{42} }
|
58
|
+
it { Mao.escape_literal(true).should eq %q{true} }
|
59
|
+
it { Mao.escape_literal(false).should eq %q{false} }
|
60
|
+
it { Mao.escape_literal(nil).should eq %q{null} }
|
61
|
+
it { Mao.escape_literal([]).should eq %q{(null)} }
|
62
|
+
it { Mao.escape_literal([1]).should eq %q{(1)} }
|
63
|
+
it { Mao.escape_literal([1, "xzy"]).should eq %q{(1, 'xzy')} }
|
64
|
+
it { Mao.escape_literal(Mao::Query.raw("\n\"'%")).should eq "\n\"'%" }
|
65
|
+
|
66
|
+
# Times are escaped to UTC always.
|
67
|
+
it { Mao.escape_literal(Time.new(2012, 11, 11, 6, 45, 0, 11 * 3600)).
|
68
|
+
should eq %q{'2012-11-10 19:45:00.000000'} }
|
69
|
+
it { Mao.escape_literal(Time.new(2012, 11, 10, 19, 45, 0, 0)).
|
70
|
+
should eq %q{'2012-11-10 19:45:00.000000'} }
|
71
|
+
it { Mao.escape_literal(Time.new(2012, 11, 10, 19, 45, 0.1, 0)).
|
72
|
+
should eq %q{'2012-11-10 19:45:00.100000'} }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe ".query" do
|
77
|
+
subject { Mao.query(:empty) }
|
78
|
+
it { should be_an_instance_of Mao::Query }
|
79
|
+
it { should be_frozen }
|
80
|
+
end
|
81
|
+
|
82
|
+
describe ".transaction" do
|
83
|
+
context "empty" do
|
84
|
+
before { PG::Connection.any_instance.should_receive(:exec).
|
85
|
+
with("BEGIN") }
|
86
|
+
before { PG::Connection.any_instance.should_receive(:exec).
|
87
|
+
with("COMMIT") }
|
88
|
+
it { Mao.transaction {} }
|
89
|
+
end
|
90
|
+
|
91
|
+
context "success" do
|
92
|
+
before { Mao.should_receive(:sql).with("BEGIN") }
|
93
|
+
before { Mao.should_receive(:sql).with(:some_sql).and_return :ok }
|
94
|
+
before { Mao.should_receive(:sql).with("COMMIT") }
|
95
|
+
it { Mao.transaction { Mao.sql(:some_sql) }.
|
96
|
+
should eq :ok }
|
97
|
+
end
|
98
|
+
|
99
|
+
context "failure" do
|
100
|
+
before { Mao.should_receive(:sql).with("BEGIN") }
|
101
|
+
before { Mao.should_receive(:sql).with(:some_sql).
|
102
|
+
and_raise(Exception.new) }
|
103
|
+
before { Mao.should_receive(:sql).with("ROLLBACK") }
|
104
|
+
it { expect { Mao.transaction { Mao.sql(:some_sql) }
|
105
|
+
}.to raise_exception }
|
106
|
+
end
|
107
|
+
|
108
|
+
context "rollback" do
|
109
|
+
before { Mao.should_receive(:sql).with("BEGIN") }
|
110
|
+
before { Mao.should_receive(:sql).with(:some_sql).
|
111
|
+
and_raise(Mao::Rollback) }
|
112
|
+
before { Mao.should_receive(:sql).with("ROLLBACK") }
|
113
|
+
it { expect { Mao.transaction { Mao.sql(:some_sql) }
|
114
|
+
}.to_not raise_exception }
|
115
|
+
end
|
116
|
+
|
117
|
+
context "nested transactions" do
|
118
|
+
# Currently not supported: the inner transactions don't add transactions
|
119
|
+
# at all.
|
120
|
+
before { Mao.should_receive(:sql).with("BEGIN").once }
|
121
|
+
before { Mao.should_receive(:sql).with("ROLLBACK").once }
|
122
|
+
|
123
|
+
it do
|
124
|
+
Mao.transaction { Mao.transaction { raise Mao::Rollback } }.
|
125
|
+
should be_false
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe ".normalize_result" do
|
131
|
+
before { Mao.should_receive(:convert_type).
|
132
|
+
with("y", "zzz").and_return("q") }
|
133
|
+
it { Mao.normalize_result({"x" => "y"}, {:x => "zzz"}).
|
134
|
+
should eq({:x => "q"}) }
|
135
|
+
end
|
136
|
+
|
137
|
+
describe ".normalize_join_result" do
|
138
|
+
let(:from) { double("from") }
|
139
|
+
let(:to) { double("to") }
|
140
|
+
|
141
|
+
before { from.should_receive(:table).and_return(:from) }
|
142
|
+
before { from.should_receive(:col_types).and_return({:a => "integer"}) }
|
143
|
+
before { to.should_receive(:table).and_return(:to) }
|
144
|
+
before { to.should_receive(:col_types).
|
145
|
+
and_return({:b => "character varying"}) }
|
146
|
+
|
147
|
+
it { Mao.normalize_join_result(
|
148
|
+
{"c1" => "1", "c2" => "2"}, from, to).
|
149
|
+
should eq({:from => {:a => 1},
|
150
|
+
:to => {:b => "2"}}) }
|
151
|
+
|
152
|
+
it { Mao.normalize_join_result(
|
153
|
+
{"c1" => "1"}, from, to).
|
154
|
+
should eq({:from => {:a => 1}}) }
|
155
|
+
end
|
156
|
+
|
157
|
+
describe ".convert_type" do
|
158
|
+
context "integers" do
|
159
|
+
it { Mao.convert_type(nil, "integer").should be_nil }
|
160
|
+
it { Mao.convert_type("42", "integer").should eq 42 }
|
161
|
+
it { Mao.convert_type("42", "smallint").should eq 42 }
|
162
|
+
it { Mao.convert_type("42", "bigint").should eq 42 }
|
163
|
+
it { Mao.convert_type("42", "serial").should eq 42 }
|
164
|
+
it { Mao.convert_type("42", "bigserial").should eq 42 }
|
165
|
+
end
|
166
|
+
|
167
|
+
context "character" do
|
168
|
+
it { Mao.convert_type(nil, "character varying").should be_nil }
|
169
|
+
it { Mao.convert_type("blah", "character varying").
|
170
|
+
should eq "blah" }
|
171
|
+
it { Mao.convert_type("blah", "character varying").encoding.
|
172
|
+
should be Encoding::UTF_8 }
|
173
|
+
|
174
|
+
it { Mao.convert_type(nil, "character varying(200)").should be_nil }
|
175
|
+
it { Mao.convert_type("blah", "character varying(200)").
|
176
|
+
should eq "blah" }
|
177
|
+
it { Mao.convert_type("blah", "character varying(200)").encoding.
|
178
|
+
should be Encoding::UTF_8 }
|
179
|
+
|
180
|
+
it { Mao.convert_type(nil, "text").should be_nil }
|
181
|
+
it { Mao.convert_type("blah", "text").
|
182
|
+
should eq "blah" }
|
183
|
+
it { Mao.convert_type("blah", "text").encoding.
|
184
|
+
should be Encoding::UTF_8 }
|
185
|
+
end
|
186
|
+
|
187
|
+
context "dates" do
|
188
|
+
it { Mao.convert_type(nil, "timestamp without time zone").
|
189
|
+
should be_nil }
|
190
|
+
# Note: without timezone is assumed to be in UTC.
|
191
|
+
it { Mao.convert_type("2012-11-10 19:45:00",
|
192
|
+
"timestamp without time zone").
|
193
|
+
should eq Time.new(2012, 11, 10, 19, 45, 0, 0) }
|
194
|
+
it { Mao.convert_type("2012-11-10 19:45:00.1",
|
195
|
+
"timestamp without time zone").
|
196
|
+
should eq Time.new(2012, 11, 10, 19, 45, 0.1, 0) }
|
197
|
+
end
|
198
|
+
|
199
|
+
context "booleans" do
|
200
|
+
it { Mao.convert_type(nil, "boolean").should be_nil }
|
201
|
+
it { Mao.convert_type("t", "boolean").should eq true }
|
202
|
+
it { Mao.convert_type("f", "boolean").should eq false }
|
203
|
+
end
|
204
|
+
|
205
|
+
context "bytea" do
|
206
|
+
it { Mao.convert_type(nil, "bytea").should be_nil }
|
207
|
+
it { Mao.convert_type("\\x5748415400", "bytea").should eq "WHAT\x00" }
|
208
|
+
it { Mao.convert_type("\\x5748415400", "bytea").encoding.
|
209
|
+
should eq Encoding::ASCII_8BIT }
|
210
|
+
end
|
211
|
+
|
212
|
+
context "numeric" do
|
213
|
+
it { Mao.convert_type(nil, "numeric").should be_nil }
|
214
|
+
it { Mao.convert_type("1234567890123456.789", "numeric").
|
215
|
+
should eq BigDecimal.new("1234567890123456.789") }
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# vim: set sw=2 cc=80 et:
|