arc 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/Gemfile +21 -0
- data/Rakefile +35 -0
- data/arc.gemspec +20 -0
- data/lib/arc.rb +13 -0
- data/lib/arc/casting.rb +66 -0
- data/lib/arc/data_stores.rb +9 -0
- data/lib/arc/data_stores/abstract/arel_compatibility.rb +50 -0
- data/lib/arc/data_stores/abstract/object_definitions.rb +55 -0
- data/lib/arc/data_stores/abstract/store.rb +55 -0
- data/lib/arc/data_stores/mysql/object_definitions.rb +54 -0
- data/lib/arc/data_stores/mysql/store.rb +51 -0
- data/lib/arc/data_stores/postgres/object_definitions.rb +70 -0
- data/lib/arc/data_stores/postgres/store.rb +38 -0
- data/lib/arc/data_stores/sqlite/object_definitions.rb +51 -0
- data/lib/arc/data_stores/sqlite/store.rb +43 -0
- data/lib/arc/quoting.rb +91 -0
- data/lib/arc/reactor.rb +29 -0
- data/lib/arc/version.rb +3 -0
- data/spec/casting_spec.rb +74 -0
- data/spec/data_stores/abstract/arel_compatibility_spec.rb +63 -0
- data/spec/data_stores/abstract/object_definitions_spec.rb +75 -0
- data/spec/data_stores/abstract/store_spec.rb +59 -0
- data/spec/data_stores/integration/crud_spec.rb +79 -0
- data/spec/data_stores/integration/schema_spec.rb +47 -0
- data/spec/data_stores/mysql/store_spec.rb +11 -0
- data/spec/data_stores/postgres/store_spec.rb +11 -0
- data/spec/data_stores/sqlite/store_spec.rb +11 -0
- data/spec/data_stores_spec.rb +15 -0
- data/spec/quoting_spec.rb +97 -0
- data/spec/spec_helper.rb +81 -0
- data/spec/support/config.yml +16 -0
- data/spec/support/resources/rails.png +0 -0
- data/spec/support/schemas/drop_mysql.sql +1 -0
- data/spec/support/schemas/drop_postgres.sql +1 -0
- data/spec/support/schemas/drop_sqlite.sql +1 -0
- data/spec/support/schemas/mysql.sql +10 -0
- data/spec/support/schemas/postgres.sql +12 -0
- data/spec/support/schemas/sqlite.sql +12 -0
- metadata +105 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
-d
|
data/Gemfile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
#this needs to get added to gemspec eventually
|
3
|
+
gem 'q', :path => '~/Projects/jacobsimeon/q'
|
4
|
+
|
5
|
+
group :test do
|
6
|
+
gem 'arel', :path => "../arel"
|
7
|
+
gem 'rspec'
|
8
|
+
gem 'simplecov', '>= 0.4.0', :require => false
|
9
|
+
gem 'ruby-debug19', :require => 'ruby-debug'
|
10
|
+
group :sqlite do
|
11
|
+
gem 'sqlite3'
|
12
|
+
end
|
13
|
+
group :postgres do
|
14
|
+
gem 'pg'
|
15
|
+
end
|
16
|
+
group :mysql do
|
17
|
+
gem 'mysql2'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
gemspec
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
task :test do
|
3
|
+
['sqlite', 'postgres', 'mysql'].each do |environment|
|
4
|
+
puts "Running tests for environment #{environment}"
|
5
|
+
ENV['ARC_ENV'] = environment
|
6
|
+
system 'rspec spec'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
task :setup do
|
11
|
+
#setup postgres
|
12
|
+
`createdb arc_development -O jacob`
|
13
|
+
puts "Setup complete"
|
14
|
+
end
|
15
|
+
|
16
|
+
namespace :test do
|
17
|
+
task :adapter, :env do |task, args|
|
18
|
+
ENV['ARC_ENV'] = args.env
|
19
|
+
system 'rspec spec/data_stores/integration/crud_spec.rb spec/data_stores/integration/schema_spec.rb'
|
20
|
+
end
|
21
|
+
task :docs do
|
22
|
+
`rspec spec -f h > doc/spec.html`
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#example use of passing arguments to rake
|
27
|
+
# task :say, :word, :person do |task, args|
|
28
|
+
# puts task
|
29
|
+
# puts '"args.word"'
|
30
|
+
# puts "\t- #{args.person}"
|
31
|
+
# end
|
32
|
+
# $ rake say[hello,"Jacob Morris"]
|
33
|
+
# >> say
|
34
|
+
# >> "hello"
|
35
|
+
# >> - Jacob Morris
|
data/arc.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "arc/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "arc"
|
7
|
+
s.version = Arc::VERSION
|
8
|
+
s.authors = ["Jacob Morris"]
|
9
|
+
s.email = ["jacob.s.morris@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{A lightweight driver for multiple sql backends}
|
12
|
+
s.description = %q{Compatible with mysql, sqlite, and postgres}
|
13
|
+
|
14
|
+
s.rubyforge_project = "arc"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
end
|
data/lib/arc.rb
ADDED
data/lib/arc/casting.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module Arc
|
2
|
+
module Casting
|
3
|
+
class CannotCastValueError < StandardError; end
|
4
|
+
|
5
|
+
TYPES = {
|
6
|
+
integer: 'integer',
|
7
|
+
int: 'integer',
|
8
|
+
char: 'string' ,
|
9
|
+
varchar: 'string' ,
|
10
|
+
binary: 'binary' ,
|
11
|
+
date: 'date' ,
|
12
|
+
datetime: 'time' ,
|
13
|
+
time: 'time' ,
|
14
|
+
bool: 'boolean',
|
15
|
+
boolean: 'boolean',
|
16
|
+
bit: 'boolean',
|
17
|
+
float: 'float' ,
|
18
|
+
}
|
19
|
+
|
20
|
+
CAST_METHODS = Hash.new do |cast_methods, key|
|
21
|
+
method = "cast_#{TYPES[key.to_sym]}"
|
22
|
+
cast_methods[key] = method
|
23
|
+
end
|
24
|
+
|
25
|
+
def cast value, type
|
26
|
+
method = CAST_METHODS[type]
|
27
|
+
return send method, value if respond_to? method, :include_private
|
28
|
+
return cast_string value
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def cast_string value
|
33
|
+
value.to_s
|
34
|
+
end
|
35
|
+
def cast_integer value
|
36
|
+
value.to_i
|
37
|
+
end
|
38
|
+
def cast_float value
|
39
|
+
value.to_f
|
40
|
+
end
|
41
|
+
def cast_time value
|
42
|
+
Time.parse value
|
43
|
+
end
|
44
|
+
def cast_date value
|
45
|
+
Date.parse value
|
46
|
+
end
|
47
|
+
def cast_binary value
|
48
|
+
value.to_s.force_encoding "BINARY"
|
49
|
+
value.gsub!(/%00|%25|\\\\/n) do |byte|
|
50
|
+
case byte
|
51
|
+
when "%00" then "\0"
|
52
|
+
when "%25" then "%"
|
53
|
+
when "\\\\" then "\\"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
def cast_boolean value
|
58
|
+
case value
|
59
|
+
when true, 1, '1', 'true', 't'
|
60
|
+
true
|
61
|
+
when false, 0, '0', 'false', 'f'
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'arel'
|
3
|
+
|
4
|
+
module Arel
|
5
|
+
module Visitors
|
6
|
+
VISITORS['postgres'] = VISITORS['postgresql']
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module Arc
|
11
|
+
module DataStores
|
12
|
+
module ArelCompatibility
|
13
|
+
# tell arel how we'll be using it today
|
14
|
+
def visitor
|
15
|
+
Arel::Visitors.for self
|
16
|
+
end
|
17
|
+
def table_exists? name
|
18
|
+
!self[name.to_sym].nil?
|
19
|
+
end
|
20
|
+
# hashes, imo, have no business in a sql database
|
21
|
+
# add ability to quote a hash here to keep arel tests passing
|
22
|
+
def quote_hash h
|
23
|
+
quote_string h.to_yaml
|
24
|
+
end
|
25
|
+
# provide a method for getting at the primary key of a given table
|
26
|
+
def primary_key table
|
27
|
+
return nil unless table
|
28
|
+
table = self[table]
|
29
|
+
pk = table.column_names.find{ |c| table[c].primary_key? }
|
30
|
+
end
|
31
|
+
# the interface for each of these methods is already implemented by the existing class
|
32
|
+
# just return self
|
33
|
+
def columns_hash
|
34
|
+
self
|
35
|
+
end
|
36
|
+
def connection
|
37
|
+
self
|
38
|
+
end
|
39
|
+
def connection_pool
|
40
|
+
self
|
41
|
+
end
|
42
|
+
def spec
|
43
|
+
self
|
44
|
+
end
|
45
|
+
def with_connection
|
46
|
+
yield self
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'q/collector'
|
2
|
+
module Arc
|
3
|
+
module DataStores
|
4
|
+
module ObjectDefinitions
|
5
|
+
|
6
|
+
class Schema
|
7
|
+
include Collector
|
8
|
+
alias :table_names :keys
|
9
|
+
def initialize store
|
10
|
+
@data_store = store
|
11
|
+
end
|
12
|
+
def execute_ddl ddl
|
13
|
+
case ddl
|
14
|
+
when Array
|
15
|
+
ddl.each { |s| @data_store.execute s }
|
16
|
+
when String
|
17
|
+
execute_ddl ddl.split(';')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Table
|
23
|
+
include Collector
|
24
|
+
alias :column_names :keys
|
25
|
+
attr_reader :name
|
26
|
+
|
27
|
+
def initialize name, store=nil
|
28
|
+
@name, @data_store = name.to_s, store
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Column
|
33
|
+
attr_reader :name
|
34
|
+
attr_reader :default
|
35
|
+
def initialize store, properties={}
|
36
|
+
@data_store = store
|
37
|
+
@name = properties.delete :name
|
38
|
+
@allows_null = properties.delete :allows_null
|
39
|
+
@default = properties.delete :default
|
40
|
+
@is_pk = properties.delete(:pk) || properties.delete(:primary_key)
|
41
|
+
@stype = properties.delete(:type)
|
42
|
+
end
|
43
|
+
def pk?; @is_pk; end
|
44
|
+
alias :primary_key? :pk?
|
45
|
+
def type
|
46
|
+
raise NotImplementedError
|
47
|
+
end
|
48
|
+
def allows_null?
|
49
|
+
@allows_null
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'arc/data_stores/abstract/arel_compatibility'
|
2
|
+
require 'arc/data_stores/abstract/object_definitions'
|
3
|
+
require 'arc/quoting'
|
4
|
+
|
5
|
+
module Arc
|
6
|
+
module DataStores
|
7
|
+
class AbstractDataStore < ResourcePool
|
8
|
+
include ArelCompatibility
|
9
|
+
include Arc::Quoting
|
10
|
+
|
11
|
+
def [] table
|
12
|
+
schema[table]
|
13
|
+
end
|
14
|
+
|
15
|
+
def schema
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def create query
|
20
|
+
#add new data
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def read query
|
25
|
+
#read existing data
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def update query
|
30
|
+
#update existing data
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
def destroy query
|
35
|
+
#destroy existing data
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute query
|
40
|
+
#adapters should override this method to execute a query against the database
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
alias :with_store :with_resource
|
44
|
+
alias :columns :[]
|
45
|
+
private
|
46
|
+
#better semantics for a class that deals with 'connections' instead of 'resources'
|
47
|
+
def create_connection
|
48
|
+
raise NotImplementedError
|
49
|
+
end
|
50
|
+
def create_resource
|
51
|
+
create_connection
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'arc/data_stores/abstract/object_definitions'
|
2
|
+
|
3
|
+
module Arc
|
4
|
+
module DataStores
|
5
|
+
module ObjectDefinitions
|
6
|
+
class MysqlSchema < Schema
|
7
|
+
def fetch_keys
|
8
|
+
@data_store.read("SHOW TABLES").map do |r|
|
9
|
+
r[r.keys.first].to_sym
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch_item name
|
14
|
+
MysqlTable.new name, @data_store
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class MysqlTable < Table
|
19
|
+
def raw_column_data
|
20
|
+
@data_store.read("SHOW FULL FIELDS FROM superheros").each do |r|
|
21
|
+
r
|
22
|
+
end
|
23
|
+
end
|
24
|
+
def fetch_keys
|
25
|
+
raw_column_data.map {|c| c[:Field].to_sym }
|
26
|
+
end
|
27
|
+
def fetch_item name
|
28
|
+
return {} unless keys.include? name.to_sym
|
29
|
+
c = raw_column_data.find{|c| c[:Field] == name.to_s}
|
30
|
+
MysqlColumn.new @data_store,{
|
31
|
+
name: c[:Field],
|
32
|
+
allows_null: c[:Null] == "YES",
|
33
|
+
default: c[:Default],
|
34
|
+
primary_key: c[:Key] == "PRI",
|
35
|
+
type: c[:Type]
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class MysqlColumn < Column
|
41
|
+
TYPES = {
|
42
|
+
:int => :integer,
|
43
|
+
}
|
44
|
+
def type
|
45
|
+
@type ||= begin
|
46
|
+
type_key = @stype.downcase.gsub(/\([\d,]*\)/, '').to_sym
|
47
|
+
TYPES[type_key] || type_key
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'mysql2'
|
2
|
+
require 'arc/data_stores/mysql/object_definitions'
|
3
|
+
|
4
|
+
Mysql2::Client.default_query_options.merge!({
|
5
|
+
:symbolize_keys => true
|
6
|
+
})
|
7
|
+
|
8
|
+
module Arc::DataStores
|
9
|
+
class MysqlDataStore < AbstractDataStore
|
10
|
+
|
11
|
+
def read query
|
12
|
+
execute(query)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create sql
|
16
|
+
table = sql.match(/\AINSERT into ([^ (]*)/i)[1]
|
17
|
+
execute sql
|
18
|
+
read("select * from #{table} where id = " + last_row_id.to_s).first
|
19
|
+
end
|
20
|
+
|
21
|
+
def update sql
|
22
|
+
execute sql
|
23
|
+
end
|
24
|
+
|
25
|
+
def destroy sql
|
26
|
+
execute sql
|
27
|
+
end
|
28
|
+
|
29
|
+
def execute query
|
30
|
+
with_store do |store|
|
31
|
+
store.query query
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def schema
|
36
|
+
@schema ||= ObjectDefinitions::MysqlSchema.new self
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def create_connection
|
41
|
+
Mysql2::Client.new @config
|
42
|
+
end
|
43
|
+
|
44
|
+
def last_row_id
|
45
|
+
with_store do |store|
|
46
|
+
store.last_id
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|