mao 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|