mystic 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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/bin/mystic +43 -0
- data/lib/mystic.rb +183 -0
- data/lib/mystic/adapter.rb +145 -0
- data/lib/mystic/adapters/abstract.rb +90 -0
- data/lib/mystic/adapters/mysql.rb +51 -0
- data/lib/mystic/adapters/postgres.rb +95 -0
- data/lib/mystic/constants.rb +11 -0
- data/lib/mystic/extensions.rb +99 -0
- data/lib/mystic/migration.rb +108 -0
- data/lib/mystic/model.rb +130 -0
- data/lib/mystic/sql.rb +244 -0
- metadata +99 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e7f8ba8f872bed5ad3946fb3eca8373a5262c820
|
4
|
+
data.tar.gz: 874d9a10179f9c9e15a6fffb7451ff03ad70788a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c10435a23302bdf3e5aec546b164b4dc73a021b9d12221e2e1387e26086a2db21f603896aa3ef0b49daaa464438d1cec143861ecac0cb7421e057c9489cfd98a
|
7
|
+
data.tar.gz: 5333e387710b4b0d19b81664359becf2a79f101e9d655820d256388a4488543b3587219514e4a2d2c212d931cb7806ee522546e75f974b2f838cdd8eb19af919
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) <year> <copyright holders>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/bin/mystic
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "mystic"
|
4
|
+
require "irb"
|
5
|
+
|
6
|
+
# Examples:
|
7
|
+
# mystic create migration InitialMigration
|
8
|
+
# mystic migrate development
|
9
|
+
# mystic rollback production
|
10
|
+
|
11
|
+
ErrorOutput = Class.new StandardError
|
12
|
+
|
13
|
+
begin
|
14
|
+
case ARGV[0].to_sym
|
15
|
+
when :create
|
16
|
+
case ARGV[1].to_sym
|
17
|
+
when :migration
|
18
|
+
raise ErrorOutput, "No migration name provided" if ARGV[2].nil?
|
19
|
+
Mystic.create_migration ARGV[2].dup
|
20
|
+
end
|
21
|
+
else
|
22
|
+
begin
|
23
|
+
Mystic.connect ARGV[1]
|
24
|
+
case ARGV[0].to_sym
|
25
|
+
when :migrate
|
26
|
+
Mystic.migrate
|
27
|
+
when :rollback
|
28
|
+
Mystic.rollback
|
29
|
+
when :console
|
30
|
+
puts "Starting Mystic console: #{Mystic.env}"
|
31
|
+
IRB.setup nil
|
32
|
+
IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
|
33
|
+
require "irb/ext/multi-irb"
|
34
|
+
IRB.irb nil, IRB::WorkSpace.new
|
35
|
+
end
|
36
|
+
Mystic.disconnect
|
37
|
+
rescue Mystic::EnvironmentError => e
|
38
|
+
puts e.message
|
39
|
+
end
|
40
|
+
end
|
41
|
+
rescue ErrorOutput => e
|
42
|
+
puts e.message
|
43
|
+
end
|
data/lib/mystic.rb
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "pathname"
|
5
|
+
require "mystic/extensions"
|
6
|
+
require "mystic/constants"
|
7
|
+
require "mystic/sql"
|
8
|
+
require "mystic/adapter"
|
9
|
+
require "mystic/migration"
|
10
|
+
require "mystic/model"
|
11
|
+
|
12
|
+
module Mystic
|
13
|
+
@@adapter = nil
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def adapter
|
17
|
+
@@adapter
|
18
|
+
end
|
19
|
+
|
20
|
+
# Mystic.connect
|
21
|
+
# Connects to a database. It's recommended you use it like ActiveRecord::Base.establish_connection
|
22
|
+
# Arguments:
|
23
|
+
# env - The env from database.yml you wish to use
|
24
|
+
def connect(env="")
|
25
|
+
load_env
|
26
|
+
@@env = (env || ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development").to_s
|
27
|
+
path = root.join("config","database.yml").to_s
|
28
|
+
db_yml = YAML.load_file path
|
29
|
+
|
30
|
+
raise EnvironmentError, "Environment '#{@@env}' doesn't exist." unless db_yml.member? @@env
|
31
|
+
|
32
|
+
conf = db_yml[@@env].symbolize
|
33
|
+
conf[:dbname] = conf[:database]
|
34
|
+
|
35
|
+
@@adapter = Adapter.create(
|
36
|
+
conf[:adapter],
|
37
|
+
:pool_size => conf[:pool_size].to_i,
|
38
|
+
:pool_timeout => conf[:timeout].to_i,
|
39
|
+
:pool_expires => conf[:expires].to_i
|
40
|
+
)
|
41
|
+
|
42
|
+
@@adapter.connect conf
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :env=, :connect
|
47
|
+
|
48
|
+
def env
|
49
|
+
@@env
|
50
|
+
end
|
51
|
+
|
52
|
+
# Mystic.disconnect
|
53
|
+
# Disconnects from the connected database. Use it like ActiveRecord::Base.connection.disconnect!
|
54
|
+
def disconnect
|
55
|
+
@@adapter.disconnect
|
56
|
+
@@adapter = nil
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
# Mystic.execute
|
61
|
+
# Execute some sql. It will be densified (the densify gem) and sent to the DB
|
62
|
+
# Arguments:
|
63
|
+
# sql - The SQL to execute
|
64
|
+
# Returns: Native Ruby objects representing the response from the DB (Usually an Array of Hashes)
|
65
|
+
def execute(sql="")
|
66
|
+
raise AdapterError, "Adapter is nil, so Mystic is not connected." if @@adapter.nil?
|
67
|
+
@@adapter.execute sql.sql_terminate.densify
|
68
|
+
end
|
69
|
+
|
70
|
+
# Mystic.sanitize
|
71
|
+
# Escape a string so that it can be used safely as input. Mystic does not support statement preparation, so this is a must.
|
72
|
+
# Arguments:
|
73
|
+
# str - The string to sanitize
|
74
|
+
# Returns: the sanitized string
|
75
|
+
def sanitize(str="")
|
76
|
+
raise AdapterError, "Adapter is nil, so Mystic is not connected." if @@adapter.nil?
|
77
|
+
@@adapter.sanitize str
|
78
|
+
end
|
79
|
+
|
80
|
+
# Mystic.root
|
81
|
+
# Get the app root
|
82
|
+
# Aguments:
|
83
|
+
# To be ignored
|
84
|
+
# Returns:
|
85
|
+
# A pathname to the application's root
|
86
|
+
def root(path=Pathname.new(Dir.pwd))
|
87
|
+
raise RootError, "Failed to find the application's root." if path == path.parent
|
88
|
+
mystic_path = path.join "config", "database.yml"
|
89
|
+
return path if mystic_path.file? # exist? is implicit with file?
|
90
|
+
root path.parent
|
91
|
+
end
|
92
|
+
|
93
|
+
# TODO: Make this a migration
|
94
|
+
# TODO: Silence this
|
95
|
+
# Mystic.create_table
|
96
|
+
# Create migration tracking table
|
97
|
+
def create_mig_table
|
98
|
+
execute "CREATE TABLE IF NOT EXISTS mystic_migrations (mig_number integer, filename text)"
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Command line
|
103
|
+
#
|
104
|
+
|
105
|
+
# Runs every yet-to-be-ran migration
|
106
|
+
def migrate
|
107
|
+
create_mig_table
|
108
|
+
migrated_filenames = execute("SELECT filename FROM mystic_migrations").map{ |r| r["filename"] }
|
109
|
+
mp = root.join("mystic","migrations").to_s
|
110
|
+
|
111
|
+
Dir.entries(mp)
|
112
|
+
.reject{ |e| MIG_REGEX.match(e).nil? }
|
113
|
+
.reject{ |e| migrated_filenames.include? e }
|
114
|
+
.sort{ |a,b| MIG_REGEX.match(a)[:num].to_i <=> MIG_REGEX.match(b)[:num].to_i }
|
115
|
+
.each{ |fname|
|
116
|
+
load File.join mp,fname
|
117
|
+
|
118
|
+
mig_num,mig_name = MIG_REGEX.match(fname).captures
|
119
|
+
Object.const_get(mig_name).new.migrate
|
120
|
+
execute "INSERT INTO mystic_migrations (mig_number, filename) VALUES (#{mig_num},'#{fname}')"
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
# Rolls back a single migration
|
125
|
+
def rollback
|
126
|
+
create_mig_table
|
127
|
+
fname = execute("SELECT filename FROM mystic_migrations ORDER BY mig_number DESC LIMIT 1")[0]["filename"] rescue nil
|
128
|
+
return if fname.nil?
|
129
|
+
|
130
|
+
load root.join("mystic","migrations",fname).to_s
|
131
|
+
|
132
|
+
mig_num,mig_name = MIG_REGEX.match(fname).captures
|
133
|
+
|
134
|
+
Object.const_get(mig_name).new.rollback
|
135
|
+
|
136
|
+
execute "DELETE FROM mystic_migrations WHERE filename='#{fname}' and mig_number=#{mig_num}"
|
137
|
+
end
|
138
|
+
|
139
|
+
# Creates a blank migration in mystic/migrations
|
140
|
+
def create_migration(name="")
|
141
|
+
name.strip!
|
142
|
+
raise CLIError, "Migration name must not be empty." if name.empty?
|
143
|
+
|
144
|
+
name[0] = name[0].capitalize
|
145
|
+
|
146
|
+
migs = root.join "mystic","migrations"
|
147
|
+
|
148
|
+
num = migs.entries.map{ |e| MIG_REGEX.match(e.to_s)[:num].to_i rescue 0 }.max.to_i+1
|
149
|
+
|
150
|
+
File.open(migs.join("#{num}_#{name}.rb").to_s, 'w') { |f| f.write(template name) }
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
# Loads the .env file
|
156
|
+
def load_env
|
157
|
+
root.join(".env").read
|
158
|
+
.split("\n")
|
159
|
+
.map { |l| l.strip.split "=", 2 }
|
160
|
+
.each { |k,v| ENV[k] = v }
|
161
|
+
end
|
162
|
+
|
163
|
+
# Retuns a blank migration's code in a String
|
164
|
+
def template(name=nil)
|
165
|
+
raise ArgumentError, "Migrations must have a name." if name.nil?
|
166
|
+
<<-mig_template
|
167
|
+
#!/usr/bin/env ruby
|
168
|
+
|
169
|
+
require "mystic"
|
170
|
+
|
171
|
+
class #{name} < Mystic::Migration
|
172
|
+
def up
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
def down
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
180
|
+
mig_template
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "densify"
|
4
|
+
require "access_stack"
|
5
|
+
|
6
|
+
module Mystic
|
7
|
+
class Adapter
|
8
|
+
attr_accessor :pool_size,
|
9
|
+
:pool_timeout,
|
10
|
+
:pool_expires
|
11
|
+
|
12
|
+
@@blocks = {}
|
13
|
+
|
14
|
+
def self.create(name="",opts={})
|
15
|
+
name = name.to_s.downcase.strip
|
16
|
+
name = "postgres" if name =~ /^postg.*$/i # Includes PostGIS
|
17
|
+
name = "mysql" if name =~ /^mysql.*$/i
|
18
|
+
|
19
|
+
require "mystic/adapters/" + name
|
20
|
+
|
21
|
+
Object.const_get("Mystic::#{name.capitalize}Adapter").new opts
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(opts={})
|
25
|
+
opts.symbolize.each do |k,v|
|
26
|
+
k = ('@' + k.to_s).to_sym
|
27
|
+
instance_variable_set k,v if instance_variables.include? k
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Gets the adapter name (examples: postgres, mysql)
|
32
|
+
def self.adapter
|
33
|
+
a = name.split("::").last.sub("Adapter","").downcase
|
34
|
+
a = "abstract" if a.empty?
|
35
|
+
a
|
36
|
+
end
|
37
|
+
|
38
|
+
def adapter
|
39
|
+
self.class.adapter
|
40
|
+
end
|
41
|
+
|
42
|
+
# Map a block to an adapter's block hash
|
43
|
+
# This avoids cases where a subclass' class var
|
44
|
+
# changes that class var on its superclass
|
45
|
+
def self.map_block(key, block)
|
46
|
+
@@blocks[adapter] ||= {}
|
47
|
+
@@blocks[adapter][key] = block
|
48
|
+
end
|
49
|
+
|
50
|
+
# Fetch a block for the current adapter
|
51
|
+
def block_for(key)
|
52
|
+
b = @@blocks[adapter][key] rescue nil
|
53
|
+
b ||= @@blocks["abstract"][key] rescue nil
|
54
|
+
b ||= lambda { "" }
|
55
|
+
b
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Adapter DSL
|
60
|
+
# Implemented using method_missing
|
61
|
+
#
|
62
|
+
|
63
|
+
# example
|
64
|
+
# execute do |inst,sql|
|
65
|
+
# inst.exec sql
|
66
|
+
# end
|
67
|
+
|
68
|
+
# execute(inst,sql)
|
69
|
+
# Executes SQL and returns Ruby types
|
70
|
+
|
71
|
+
# TODO: different kinds of escaping: ident & quote
|
72
|
+
|
73
|
+
# sanitize(inst,sql)
|
74
|
+
# Escapes a literal
|
75
|
+
|
76
|
+
# connect(opts)
|
77
|
+
# Creates an instance of the DB connection
|
78
|
+
|
79
|
+
# disconnect(inst)
|
80
|
+
# Disconnects and destroys inst (a database connection)
|
81
|
+
|
82
|
+
# validate(inst)
|
83
|
+
# Checks if inst is a valid connection
|
84
|
+
|
85
|
+
# Map missing methods to blocks
|
86
|
+
# DB ops or SQL generation
|
87
|
+
def self.method_missing(meth, *args, &block)
|
88
|
+
map_block meth, block
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Adapter methods
|
93
|
+
# These are called internally
|
94
|
+
# They are called by class methods
|
95
|
+
# in ../mystic.rb
|
96
|
+
#
|
97
|
+
def connect(opts)
|
98
|
+
disconnect unless @pool.nil?
|
99
|
+
@pool = AccessStack.new(
|
100
|
+
:size => @pool_size,
|
101
|
+
:timeout => @pool_timeout,
|
102
|
+
:expires => @pool_expires,
|
103
|
+
:create => lambda { block_for(:connect).call opts }
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def disconnect
|
108
|
+
@pool.destroy ||= block_for :disconnect
|
109
|
+
@pool.empty!
|
110
|
+
end
|
111
|
+
|
112
|
+
def reap
|
113
|
+
@pool.validate ||= block_for :validate
|
114
|
+
@pool.reap
|
115
|
+
end
|
116
|
+
|
117
|
+
def execute(sql)
|
118
|
+
raise AdapterError, "Adapter's connection pool doesn't exist and so Mystic has not connected to the database." if @pool.nil?
|
119
|
+
@pool.with { |inst| block_for(:execute).call inst, sql }
|
120
|
+
end
|
121
|
+
|
122
|
+
def sanitize(str)
|
123
|
+
@pool.with { |inst| block_for(:sanitize).call inst, str }
|
124
|
+
end
|
125
|
+
|
126
|
+
def json_supported?
|
127
|
+
block_for(:json_supported).call
|
128
|
+
end
|
129
|
+
|
130
|
+
def serialize_sql(obj)
|
131
|
+
return case obj
|
132
|
+
when SQL::Table
|
133
|
+
block_for(:table).call obj
|
134
|
+
when SQL::Index
|
135
|
+
block_for(:index).call obj
|
136
|
+
when SQL::Column
|
137
|
+
block_for(:column).call obj
|
138
|
+
when SQL::Operation
|
139
|
+
res = block_for(obj.kind).call obj
|
140
|
+
obj.callback.call unless obj.callback.nil?
|
141
|
+
res
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "mystic"
|
4
|
+
require "mystic/adapter"
|
5
|
+
require "mystic/sql"
|
6
|
+
|
7
|
+
#
|
8
|
+
# This adapter is designed to hold
|
9
|
+
# basic, spec-adherent SQL generation
|
10
|
+
# so adapters can be more DRY
|
11
|
+
#
|
12
|
+
|
13
|
+
module Mystic
|
14
|
+
class Adapter
|
15
|
+
|
16
|
+
json_supported { false }
|
17
|
+
|
18
|
+
table do |obj|
|
19
|
+
sql = []
|
20
|
+
sql.push "CREATE TABLE #{obj.name} (#{obj.columns.map(&:to_sql)*","})" if obj.create?
|
21
|
+
sql.push "ALTER TABLE #{obj.name} #{obj.columns.map{|c| "ADD COLUMN #{c.to_sql}" }*', '}" unless obj.create?
|
22
|
+
sql.push *(obj.indeces.map(&:to_sql)) unless obj.indeces.empty?
|
23
|
+
sql.push *(obj.operations.map(&:to_sql)) unless obj.operations.empty?
|
24
|
+
sql*"; "
|
25
|
+
end
|
26
|
+
|
27
|
+
column do |obj|
|
28
|
+
sql = []
|
29
|
+
sql << obj.name
|
30
|
+
sql << obj.kind.downcase
|
31
|
+
sql << "(#{obj.size})" if obj.size && !obj.size.empty? && !obj.geospatial?
|
32
|
+
sql << "(#{obj.geom_kind}, #{obj.geom_srid})" if obj.geospatial?
|
33
|
+
sql << (obj.constraints[:null] ? "NULL" : "NOT NULL") if obj.constraints.member?(:null)
|
34
|
+
sql << "UNIQUE" if obj.constraints[:unique]
|
35
|
+
sql << "PRIMARY KEY" if obj.constraints[:primary_key]
|
36
|
+
sql << "REFERENCES " + obj.constraints[:references] if obj.constraints.member?(:references)
|
37
|
+
sql << "DEFAULT " + obj.constraints[:default] if obj.constraints.member?(:default)
|
38
|
+
sql << "CHECK(#{obj.constraints[:check]})" if obj.constraints.member?(:check)
|
39
|
+
sql*" "
|
40
|
+
end
|
41
|
+
|
42
|
+
index do |index|
|
43
|
+
sql = []
|
44
|
+
sql << "CREATE"
|
45
|
+
sql << "UNIQUE" if index.unique
|
46
|
+
sql << "INDEX"
|
47
|
+
sql << index.name if index.name
|
48
|
+
sql << "ON"
|
49
|
+
sql << index.table_name
|
50
|
+
sql << "(#{index.columns.map(&:to_s).join(',')})"
|
51
|
+
sql*" "
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
## Operations
|
56
|
+
#
|
57
|
+
|
58
|
+
drop_columns do |obj|
|
59
|
+
"ALTER TABLE #{obj.table_name} #{obj.column_names.map{|c| "DROP COLUMN #{c.to_s}" }*', '}"
|
60
|
+
end
|
61
|
+
|
62
|
+
rename_column do |obj|
|
63
|
+
"ALTER TABLE #{obj.table_name} RENAME COLUMN #{obj.old_name} TO #{obj.new_name}"
|
64
|
+
end
|
65
|
+
|
66
|
+
create_view do |obj|
|
67
|
+
"CREATE VIEW #{obj.name} AS #{obj.sql}"
|
68
|
+
end
|
69
|
+
|
70
|
+
drop_view do |obj|
|
71
|
+
"DROP VIEW #{obj.name}"
|
72
|
+
end
|
73
|
+
|
74
|
+
drop_table do |obj|
|
75
|
+
"DROP TABLE #{obj.table_name}"
|
76
|
+
end
|
77
|
+
|
78
|
+
rename_table do |obj|
|
79
|
+
"ALTER TABLE #{obj.old_name} RENAME TO #{obj.new_name}"
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
## Transaction Operations
|
84
|
+
#
|
85
|
+
|
86
|
+
start_transaction { "BEGIN" }
|
87
|
+
commit_transaction { "COMMIT" }
|
88
|
+
rollback_transaction { "ROLLBACK" }
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "mysql2"
|
4
|
+
|
5
|
+
# TODO:
|
6
|
+
# 1. Implement geometry
|
7
|
+
|
8
|
+
module Mystic
|
9
|
+
class MysqlAdapter < Mystic::Adapter
|
10
|
+
ALGORITHMS = [:default, :inplace, :copy]
|
11
|
+
LOCKS = [:default, :none, :shared, :exclusive]
|
12
|
+
|
13
|
+
connect { |opts| Mysql2::client.new opts }
|
14
|
+
disconnect { |mysql| mysql.close }
|
15
|
+
validate { |mysql| mysql.ping }
|
16
|
+
execute { |mysql, sql| mysql.query(sql).to_a }
|
17
|
+
sanitize { |mysql, str| mysql.escape str }
|
18
|
+
|
19
|
+
drop_index do |index|
|
20
|
+
"DROP INDEX #{index.name} ON #{index.table_name}"
|
21
|
+
end
|
22
|
+
|
23
|
+
index do |index|
|
24
|
+
sql = []
|
25
|
+
sql << "CREATE"
|
26
|
+
sql << "UNIQUE" if index.unique
|
27
|
+
sql << "INDEX"
|
28
|
+
sql << index.name if index.name
|
29
|
+
sql << "ON"
|
30
|
+
sql << index.table_name
|
31
|
+
sql << "USING #{obj.type.to_s.capitalize}" if index.type
|
32
|
+
sql << "(#{index.columns.map(&:to_s).join ',' })"
|
33
|
+
sql << "COMMENT #{index.comment.truncate 1024}" if index.comment
|
34
|
+
sql << "ALGORITHM #{index.algorithm.to_s.upcase}" if !index.lock && ALGORITHMS.include? index.algorithm
|
35
|
+
sql << "LOCK #{index.lock.to_s.upcase}" if !index.algorithm && LOCKS.include? index.lock
|
36
|
+
sql*" "
|
37
|
+
end
|
38
|
+
|
39
|
+
# Leverage MySQL savepoints to make up for transactions' flaws
|
40
|
+
start_transaction { <<-sql
|
41
|
+
BEGIN;
|
42
|
+
SAVEPOINT mystic_migration8889;
|
43
|
+
DECLARE EXIT HANDLER FOR SQLWARNING ROLLBACK TO mystic_migration8889;
|
44
|
+
DECLARE EXIT HANDLER FOR NOT FOUND ROLLBACK TO mystic_migration8889;
|
45
|
+
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK TO mystic_migration8889;
|
46
|
+
sql
|
47
|
+
}
|
48
|
+
commit_transaction { "RELEASE SAVEPOINT mystic_migration8889;COMMIT" }
|
49
|
+
rollback_transaction { "ROLLBACK TO mystic_migration8889; RELEASE SAVEPOINT mystic_migration8889; ROLLBACK" }
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "mystic/adapters/abstract"
|
4
|
+
require "pg"
|
5
|
+
|
6
|
+
# Mystic adapter for Postgres, includes PostGIS
|
7
|
+
|
8
|
+
module Mystic
|
9
|
+
class PostgresAdapter < Mystic::Adapter
|
10
|
+
INDEX_TYPES = [
|
11
|
+
:btree,
|
12
|
+
:hash,
|
13
|
+
:gist,
|
14
|
+
:spgist,
|
15
|
+
:gin
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
CONNECT_FIELDS = [
|
19
|
+
:host,
|
20
|
+
:hostaddr,
|
21
|
+
:port,
|
22
|
+
:dbname,
|
23
|
+
:user,
|
24
|
+
:password,
|
25
|
+
:connect_timeout,
|
26
|
+
:options,
|
27
|
+
:tty,
|
28
|
+
:sslmode,
|
29
|
+
:krbsrvname,
|
30
|
+
:gsslib
|
31
|
+
].freeze
|
32
|
+
|
33
|
+
connect { |opts| PG.connect opts.subhash(*CONNECT_FIELDS) }
|
34
|
+
disconnect { |pg| pg.close }
|
35
|
+
validate { |pg| pg.status == CONNECTION_OK }
|
36
|
+
sanitize { |pg, str| pg.escape_string str }
|
37
|
+
json_supported { true }
|
38
|
+
|
39
|
+
execute do |pg, sql|
|
40
|
+
res = pg.exec sql
|
41
|
+
v = res[0][Mystic::JSON_COL] if res.ntuples == 1 && res.nfields == 1
|
42
|
+
v ||= res.ntuples.times.map { |i| res[i] } unless res.nil?
|
43
|
+
v ||= []
|
44
|
+
v
|
45
|
+
end
|
46
|
+
|
47
|
+
drop_index do |index|
|
48
|
+
"DROP INDEX #{index.index_name}"
|
49
|
+
end
|
50
|
+
|
51
|
+
create_ext do |ext|
|
52
|
+
"CREATE EXTENSION \"#{ext.name}\""
|
53
|
+
end
|
54
|
+
|
55
|
+
drop_ext do |ext|
|
56
|
+
"DROP EXTENSION \"#{ext.name}\""
|
57
|
+
end
|
58
|
+
|
59
|
+
index do |index|
|
60
|
+
storage_params = index.opts.subhash :fillfactor, :buffering, :fastupdate
|
61
|
+
|
62
|
+
sql = []
|
63
|
+
sql << "CREATE"
|
64
|
+
sql << "UNIQUE" if index.unique
|
65
|
+
sql << "INDEX"
|
66
|
+
sql << "CONCURENTLY" if index.concurrently
|
67
|
+
sql << index.name unless index.name.nil?
|
68
|
+
sql << "ON #{index.table_name}"
|
69
|
+
sql << "USING #{index.type}" if INDEX_TYPES.include? index.type
|
70
|
+
sql << "(#{index.columns.map(&:to_s).join ',' })"
|
71
|
+
sql << "WITH (#{storage_params.sqlize})" unless storage_params.empty?
|
72
|
+
sql << "TABLESPACE #{index.tablespace}" unless index.tablespace.nil?
|
73
|
+
sql << "WHERE #{index.where}" unless index.where.nil?
|
74
|
+
sql*' '
|
75
|
+
end
|
76
|
+
|
77
|
+
table do |table|
|
78
|
+
sql = []
|
79
|
+
|
80
|
+
if table.create?
|
81
|
+
tbl = []
|
82
|
+
tbl << "CREATE TABLE #{table.name} (#{table.columns.map(&:to_sql)*","})"
|
83
|
+
tbl << "INHERITS #{table.inherits}" if table.inherits
|
84
|
+
tbl << "TABLESPACE #{table.tablespace}" if table.tablespace
|
85
|
+
sql << tbl*' '
|
86
|
+
else
|
87
|
+
sql << "ALTER TABLE #{table.name} #{table.columns.map{ |c| "ADD COLUMN #{c.to_sql}" }*', ' }"
|
88
|
+
end
|
89
|
+
|
90
|
+
sql.push(*table.indeces.map(&:to_sql)) unless table.indeces.empty?
|
91
|
+
sql.push(*table.operations.map(&:to_sql)) unless table.operations.empty?
|
92
|
+
sql*'; '
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module Mystic
|
4
|
+
MysticError = Class.new StandardError
|
5
|
+
RootError = Class.new StandardError
|
6
|
+
EnvironmentError = Class.new StandardError
|
7
|
+
AdapterError = Class.new StandardError
|
8
|
+
CLIError = Class.new StandardError
|
9
|
+
MIG_REGEX = /(?<num>\d+)_(?<name>[a-z]+)\.rb$/i # matches migration files (ex '1_MigrationClassName.rb')
|
10
|
+
JSON_COL = "mystic_return_json89788"
|
11
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
def self.silent
|
5
|
+
v = $VERBOSE
|
6
|
+
$VERBOSE = false
|
7
|
+
yield
|
8
|
+
$VERBOSE = v
|
9
|
+
nil
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class String
|
14
|
+
def desnake
|
15
|
+
downcase.split("_").map(&:capitalize)*' '
|
16
|
+
end
|
17
|
+
|
18
|
+
def sanitize
|
19
|
+
Mystic.sanitize(self).untaint
|
20
|
+
end
|
21
|
+
|
22
|
+
def truncate(len)
|
23
|
+
self[0..len-1]
|
24
|
+
end
|
25
|
+
|
26
|
+
def sql_terminate
|
27
|
+
return self + ";" unless dup.strip.end_with? ";"
|
28
|
+
self
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Array
|
33
|
+
def merge_keys(*keys)
|
34
|
+
raise ArgumentError, "No keys to merge." if keys.nil? || keys.empty?
|
35
|
+
raise ArgumentError, "Argument array must have the same number of elements as self." if keys.count != self.count
|
36
|
+
Hash[each_with_index.map{ |v,i| [keys[i],v] }]
|
37
|
+
end
|
38
|
+
|
39
|
+
def symbolize
|
40
|
+
map(&:to_sym)
|
41
|
+
end
|
42
|
+
|
43
|
+
def symbolize!
|
44
|
+
map!(&:to_sym)
|
45
|
+
end
|
46
|
+
|
47
|
+
def sqlize
|
48
|
+
map { |o|
|
49
|
+
case o
|
50
|
+
when String
|
51
|
+
"'#{o.sanitize}'"
|
52
|
+
when Numeric
|
53
|
+
o.to_s
|
54
|
+
end
|
55
|
+
}.compact
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Hash
|
60
|
+
def subhash(*keys)
|
61
|
+
Hash[values_at(*keys).merge_keys(*keys).reject{ |k,v| v.nil? }]
|
62
|
+
end
|
63
|
+
|
64
|
+
def parify(delim=" ")
|
65
|
+
map { |pair| pair * delim }
|
66
|
+
end
|
67
|
+
|
68
|
+
def compact
|
69
|
+
reject { |k,v| v.nil? }
|
70
|
+
end
|
71
|
+
|
72
|
+
def compact!
|
73
|
+
reject! { |k,v| v.nil? }
|
74
|
+
end
|
75
|
+
|
76
|
+
def symbolize
|
77
|
+
Hash[map { |k,v| [k.to_sym, v]}]
|
78
|
+
end
|
79
|
+
|
80
|
+
def symbolize!
|
81
|
+
keys.each { |key| self[key.to_sym] = delete key }
|
82
|
+
end
|
83
|
+
|
84
|
+
def sqlize
|
85
|
+
reject { |k,v| v.nil? || v.empty? }.map{ |k,v| "#{k}=#{Integer === v ? v : "'#{v.to_s.sanitize}'" }" }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Pathname
|
90
|
+
Kernel.silent do
|
91
|
+
def relative?
|
92
|
+
@path[0] != File::SEPARATOR
|
93
|
+
end
|
94
|
+
|
95
|
+
def join(*args)
|
96
|
+
Pathname.new(File.join @path, *args.map(&:to_s))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module Mystic
|
4
|
+
class Migration
|
5
|
+
Error = Class.new(StandardError)
|
6
|
+
IrreversibleError = Class.new(StandardError)
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@irreversible = false
|
10
|
+
@sql = ""
|
11
|
+
end
|
12
|
+
|
13
|
+
def migrate
|
14
|
+
exec_migration :up
|
15
|
+
end
|
16
|
+
|
17
|
+
def rollback
|
18
|
+
exec_migration :down
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO: This is ugly... It needs cleaning up.
|
22
|
+
def exec_migration(direction)
|
23
|
+
@sql = ""
|
24
|
+
|
25
|
+
direction = direction.to_sym
|
26
|
+
|
27
|
+
raise ArgumentError, "Direction must be either :up or :down." unless [:up, :down].include? direction
|
28
|
+
raise IrreversibleError, "Impossible to roll back an irreversible migration." if direction == :down && irreversible?
|
29
|
+
|
30
|
+
execute Mystic::SQL::Operation.start_transaction
|
31
|
+
method(direction).call
|
32
|
+
execute Mystic::SQL::Operation.commit_transaction
|
33
|
+
|
34
|
+
Mystic.adapter.execute @sql # bypass densification
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
#
|
39
|
+
# DSL
|
40
|
+
#
|
41
|
+
|
42
|
+
# All migration SQL goes through here
|
43
|
+
def execute(obj)
|
44
|
+
@sql << obj.to_s.sql_terminate # to_sql isn't defined for strings, to_sql is aliased to to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
def irreversible!
|
48
|
+
@irreversible = true
|
49
|
+
end
|
50
|
+
|
51
|
+
def irreversible?
|
52
|
+
@irreversible
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_table(name)
|
56
|
+
raise ArgumentError, "No block provided, a block is required to create a table." unless block_given?
|
57
|
+
table = Mystic::SQL::Table.create :name => name
|
58
|
+
yield table
|
59
|
+
execute table
|
60
|
+
end
|
61
|
+
|
62
|
+
def alter_table(name)
|
63
|
+
raise ArgumentError, "No block provided, a block is required to alter a table." unless block_given?
|
64
|
+
table = Mystic::SQL::Table.alter :name => name
|
65
|
+
yield table
|
66
|
+
execute table
|
67
|
+
end
|
68
|
+
|
69
|
+
def drop_table(name)
|
70
|
+
irreversible!
|
71
|
+
execute Mystic::SQL::Operation.drop_table(
|
72
|
+
:table_name => name.to_s
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def drop_index(*args)
|
77
|
+
execute Mystic::SQL::Operation.drop_index(
|
78
|
+
:index_name => args[0],
|
79
|
+
:table_name => args[1]
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def create_ext(extname)
|
84
|
+
execute Mystic::SQL::Operation.create_ext(
|
85
|
+
:name => extname.to_s
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def drop_ext(extname)
|
90
|
+
execute Mystic::SQL::Operation.drop_ext(
|
91
|
+
:name => extname.to_s
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_view(name, sql)
|
96
|
+
execute Mystic::SQL::Operation.create_view(
|
97
|
+
:name => name.to_s,
|
98
|
+
:sql => sql.to_s
|
99
|
+
)
|
100
|
+
end
|
101
|
+
|
102
|
+
def drop_view(name)
|
103
|
+
execute Mystic::SQL::Operation.drop_view(
|
104
|
+
:name => name.to_s
|
105
|
+
)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/mystic/model.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module Mystic
|
4
|
+
class Model
|
5
|
+
def self.table_name
|
6
|
+
to_s.downcase
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.visible_cols
|
10
|
+
["*"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.wrapper_sql(opts={})
|
14
|
+
sym_opts = opts.symbolize
|
15
|
+
|
16
|
+
sql = sym_opts[:sql] || "SELECT 1"
|
17
|
+
return_rows = sym_opts[:return_rows] || false
|
18
|
+
return_json = sym_opts[:return_json] || false
|
19
|
+
return_rows = true if return_json
|
20
|
+
|
21
|
+
op = sql.split(/\s+/,2).first
|
22
|
+
|
23
|
+
sql << " RETURNING #{visible_cols*','}" if return_rows && op != "SELECT"
|
24
|
+
|
25
|
+
s = []
|
26
|
+
|
27
|
+
if return_json
|
28
|
+
s << "WITH res AS (#{sql}) SELECT"
|
29
|
+
s << "row_to_json(res)" if op == "INSERT"
|
30
|
+
s << "array_to_json(array_agg(res))" unless op == "INSERT"
|
31
|
+
s << "AS #{Mystic::JSON_COL}"
|
32
|
+
s << "FROM res"
|
33
|
+
else
|
34
|
+
s << sql
|
35
|
+
end
|
36
|
+
|
37
|
+
s*' '
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.function_sql(returns_rows, funcname, *params)
|
41
|
+
"SELECT #{returns_rows ? "* FROM" : ""} #{funcname}(#{params.sqlize*','})"
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.select_sql(params={}, opts={})
|
45
|
+
sym_opts = opts.symbolize
|
46
|
+
count = sym_opts[:count] || 0
|
47
|
+
where = params.sqlize
|
48
|
+
|
49
|
+
sql = []
|
50
|
+
sql << "SELECT #{visible_cols*','} FROM #{table_name}"
|
51
|
+
sql << "WHERE #{where*' AND '}" if where.count > 0
|
52
|
+
sql << "LIMIT #{count.to_i}" if count > 0
|
53
|
+
|
54
|
+
wrapper_sql(
|
55
|
+
:sql => sql.join(' '),
|
56
|
+
:return_rows => true,
|
57
|
+
:return_json => sym_opts[:return_json] && Mystic.adapter.json_supported?
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.update_sql(where={}, set={}, opts={})
|
62
|
+
return "" if where.empty?
|
63
|
+
return "" if set.empty?
|
64
|
+
|
65
|
+
sym_opts = opts.symbolize
|
66
|
+
|
67
|
+
wrapper_sql(
|
68
|
+
:sql => "UPDATE #{table_name} SET #{set.sqlize*','} WHERE #{where.sqlize*' AND '}",
|
69
|
+
:return_rows => sym_opts[:return_rows],
|
70
|
+
:return_json => sym_opts[:return_json] && Mystic.adapter.json_supported?
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.insert_sql(params={}, opts={})
|
75
|
+
return "" if params.empty?
|
76
|
+
|
77
|
+
sym_opts = opts.symbolize
|
78
|
+
|
79
|
+
wrapper_sql(
|
80
|
+
:sql => "INSERT INTO #{table_name} (#{params.keys*','}) VALUES (#{params.values.sqlize*','})",
|
81
|
+
:return_rows => sym_opts[:return_rows],
|
82
|
+
:return_json => sym_opts[:return_json] && Mystic.adapter.json_supported?
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.delete_sql(params={}, opts={})
|
87
|
+
return "" if params.empty?
|
88
|
+
|
89
|
+
sym_opts = opts.symbolize
|
90
|
+
|
91
|
+
wrapper_sql(
|
92
|
+
:sql => "DELETE FROM #{table_name} WHERE #{params.sqlize*' AND '}",
|
93
|
+
:return_rows => sym_opts[:return_rows],
|
94
|
+
:return_json => sym_opts[:return_json] && Mystic.adapter.json_supported?
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.select(params={}, opts={})
|
99
|
+
Mystic.execute select_sql(params, opts)
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.fetch(params={}, opts={})
|
103
|
+
res = select params, opts.merge({:count => 1})
|
104
|
+
return res if res.is_a? String
|
105
|
+
res.first rescue nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.create(params={}, opts={})
|
109
|
+
res = Mystic.execute insert_sql(params, opts)
|
110
|
+
return res if res.is_a? String
|
111
|
+
res.first rescue nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.update(where={}, set={}, opts={})
|
115
|
+
Mystic.execute update_sql(where, set, opts.merge({ :return_rows => true }))
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.delete(params={}, opts={})
|
119
|
+
Mystic.execute delete_sql(params, opts)
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.exec_func(funcname, *params)
|
123
|
+
Mystic.execute function_sql(false, funcname, *params)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.exec_func_rows(funcname, *params)
|
127
|
+
Mystic.execute function_sql(true, funcname, *params)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/mystic/sql.rb
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
module Mystic
|
4
|
+
module SQL
|
5
|
+
Error = Class.new(StandardError)
|
6
|
+
|
7
|
+
class SQLObject
|
8
|
+
def to_sql
|
9
|
+
Mystic.adapter.serialize_sql self
|
10
|
+
end
|
11
|
+
|
12
|
+
alias_method :to_s, :to_sql
|
13
|
+
end
|
14
|
+
|
15
|
+
class Index < SQLObject
|
16
|
+
attr_accessor :name, # Symbol or string
|
17
|
+
:table_name, # Symbol or string
|
18
|
+
:type, # Symbol
|
19
|
+
:unique, # TrueClass/FalseClass
|
20
|
+
:columns, # Array of Strings
|
21
|
+
:opts # Hash, see below
|
22
|
+
|
23
|
+
# opts
|
24
|
+
# It's a Hash that represents options
|
25
|
+
#
|
26
|
+
# MYSQL ONLY
|
27
|
+
# Key => Value (type)
|
28
|
+
# :comment => A string that's up to 1024 chars (String)
|
29
|
+
# :algorithm => The algorithm to use (Symbol)
|
30
|
+
# :lock => The lock to use (Symbol)
|
31
|
+
#
|
32
|
+
# POSTGRES ONLY
|
33
|
+
# Key => Value (type)
|
34
|
+
# :fillfactor => A value in the range 10..100 (Integer)
|
35
|
+
# :fastupdate => true/false (TrueClass/FalseClass)
|
36
|
+
# :concurrently => true/false (TrueClass/FalseClass)
|
37
|
+
# :tablespace => The name of the desired tablespace (String)
|
38
|
+
# :buffering => :on/:off/:auto (Symbol)
|
39
|
+
# :concurrently => true/false (TrueClass/FalseClass)
|
40
|
+
# :where => The conditions for including entries in your index, same as SELECT * FROM table WHERE ____ (String)
|
41
|
+
|
42
|
+
def initialize(opts={})
|
43
|
+
opts.symbolize!
|
44
|
+
raise ArgumentError, "Indeces need a table_name or else what's the point?." unless opts.member? :table_name
|
45
|
+
raise ArgumentError, "Indeces need columns or else what's the point?" unless opts.member? :columns
|
46
|
+
@name = opts.delete(:name).to_sym if opts.member? :name
|
47
|
+
@table_name = opts.delete(:table_name).to_sym
|
48
|
+
@type = (opts.delete :type || :btree).to_s.downcase.to_sym
|
49
|
+
@unique = opts.delete :unique || false
|
50
|
+
@columns = opts.delete(:columns).symbolize rescue []
|
51
|
+
@opts = opts
|
52
|
+
end
|
53
|
+
|
54
|
+
# can accept shit other than columns like
|
55
|
+
# box(location,location)
|
56
|
+
def <<(col)
|
57
|
+
case col
|
58
|
+
when Column
|
59
|
+
@columns << col.name.to_s
|
60
|
+
when String
|
61
|
+
@columns << col
|
62
|
+
else
|
63
|
+
raise ArgumentError, "Column must be a String or a Mystic::SQL::Column"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
alias_method :push, :<<
|
68
|
+
|
69
|
+
def method_missing(meth, *args, &block)
|
70
|
+
return @opts[meth] if @opts.member? meth
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Column < SQLObject
|
76
|
+
attr_accessor :name, :kind, :size, :constraints, :geom_kind, :geom_srid
|
77
|
+
|
78
|
+
def initialize(opts={})
|
79
|
+
@name = opts.delete(:name).to_s
|
80
|
+
@kind = opts.delete(:kind).to_sym
|
81
|
+
@size = opts.delete(:size).to_s if opts.member? :size
|
82
|
+
@geom_kind = opts.delete(:geom_kind)
|
83
|
+
@geom_srid = opts.delete(:geom_srid).to_i
|
84
|
+
@constraints = opts
|
85
|
+
end
|
86
|
+
|
87
|
+
def geospatial?
|
88
|
+
@geom_kind && @geom_srid
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class Table < SQLObject
|
93
|
+
attr_reader :name
|
94
|
+
attr_accessor :columns,
|
95
|
+
:indeces,
|
96
|
+
:operations,
|
97
|
+
:opts
|
98
|
+
|
99
|
+
def self.create(opts={})
|
100
|
+
new true, opts
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.alter(opts={})
|
104
|
+
new false, opts
|
105
|
+
end
|
106
|
+
|
107
|
+
def initialize(is_create=true, opts={})
|
108
|
+
@is_create = is_create
|
109
|
+
@opts = opts.symbolize
|
110
|
+
@columns = []
|
111
|
+
@indeces = []
|
112
|
+
@operations = []
|
113
|
+
|
114
|
+
@name = @opts.delete(:name).to_s
|
115
|
+
raise ArgumentError, "Argument 'name' is invalid." if @name.empty?
|
116
|
+
end
|
117
|
+
|
118
|
+
def create?
|
119
|
+
@is_create
|
120
|
+
end
|
121
|
+
|
122
|
+
def <<(obj)
|
123
|
+
case obj
|
124
|
+
when Column
|
125
|
+
@columns << obj
|
126
|
+
when Index
|
127
|
+
@indeces << obj
|
128
|
+
when Operation
|
129
|
+
@operations << obj
|
130
|
+
else
|
131
|
+
raise ArgumentError, "Argument is not a Mystic::SQL::Column, Mystic::SQL::Operation, or Mystic::SQL::Index."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def to_sql
|
136
|
+
raise ArgumentError, "Table cannot have zero columns." if @columns.empty?
|
137
|
+
super
|
138
|
+
end
|
139
|
+
|
140
|
+
alias_method :push, :<<
|
141
|
+
|
142
|
+
#
|
143
|
+
## Operation DSL
|
144
|
+
#
|
145
|
+
|
146
|
+
def drop_index(idx_name)
|
147
|
+
raise Mystic::SQL::Error, "Cannot drop an index on a table that doesn't exist." if create?
|
148
|
+
self << Mystic::SQL::Operation.drop_index(
|
149
|
+
:index_name => idx_name.to_s,
|
150
|
+
:table_name => self.name.to_s
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
def rename_column(oldname, newname)
|
155
|
+
raise Mystic::SQL::Error, "Cannot rename a column on a table that doesn't exist." if create?
|
156
|
+
self << Mystic::SQL::Operation.rename_column(
|
157
|
+
:table_name => self.name.to_s,
|
158
|
+
:old_name => oldname.to_s,
|
159
|
+
:new_name => newname.to_s
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
def rename(newname)
|
164
|
+
raise Mystic::SQL::Error, "Cannot rename a table that doesn't exist." if create?
|
165
|
+
self << Mystic::SQL::Operation.rename_table(
|
166
|
+
:old_name => self.name.dup.to_s,
|
167
|
+
:new_name => newname.to_s,
|
168
|
+
:callback => lambda { self.name = newname }
|
169
|
+
)
|
170
|
+
end
|
171
|
+
|
172
|
+
def drop_columns(*col_names)
|
173
|
+
raise Mystic::SQL::Error, "Cannot drop a column(s) on a table that doesn't exist." if create?
|
174
|
+
self << Mystic::SQL::Operation.drop_columns(
|
175
|
+
:table_name => self.name.to_s,
|
176
|
+
:column_names => col_names.map(&:to_s)
|
177
|
+
)
|
178
|
+
end
|
179
|
+
|
180
|
+
#
|
181
|
+
## Column DSL
|
182
|
+
#
|
183
|
+
|
184
|
+
def column(col_name, kind, opts={})
|
185
|
+
self << Mystic::SQL::Column.new({
|
186
|
+
:name => col_name,
|
187
|
+
:kind => kind.to_sym
|
188
|
+
}.merge(opts || {}))
|
189
|
+
end
|
190
|
+
|
191
|
+
def geometry(col_name, kind, srid, opts={})
|
192
|
+
self << Mystic::SQL::Column.new({
|
193
|
+
:name => col_name,
|
194
|
+
:kind => :geometry,
|
195
|
+
:geom_kind => kind,
|
196
|
+
:geom_srid => srid
|
197
|
+
}.merge(opts || {}))
|
198
|
+
end
|
199
|
+
|
200
|
+
def index(*cols)
|
201
|
+
opts = cols.delete_at -1 if cols.last.is_a? Hash
|
202
|
+
opts ||= {}
|
203
|
+
opts[:columns] = cols
|
204
|
+
opts[:table_name] = @name
|
205
|
+
self << Mystic::SQL::Index.new(opts)
|
206
|
+
end
|
207
|
+
|
208
|
+
def method_missing(meth, *args, &block)
|
209
|
+
return column args[0], meth.to_s, args[1] if args.count > 0
|
210
|
+
return @opts[meth] if @opts.member?(meth)
|
211
|
+
nil
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
class Operation < SQLObject
|
216
|
+
attr_reader :kind,
|
217
|
+
:callback
|
218
|
+
|
219
|
+
def initialize(kind, opts={})
|
220
|
+
@kind = kind
|
221
|
+
@opts = opts.dup
|
222
|
+
@callback = @opts.delete :callback
|
223
|
+
end
|
224
|
+
|
225
|
+
def method_missing(meth, *args, &block)
|
226
|
+
@opts[meth.to_s.to_sym] rescue nil
|
227
|
+
end
|
228
|
+
|
229
|
+
def self.method_missing(meth, *args, &block)
|
230
|
+
new meth, (args[0] || {})
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
class Raw < SQLObject
|
235
|
+
def initialize(opts)
|
236
|
+
@sql = opts[:sql]
|
237
|
+
end
|
238
|
+
|
239
|
+
def to_sql
|
240
|
+
@sql
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
metadata
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mystic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nathaniel Symer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-07-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: densify
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: access_stack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3'
|
55
|
+
description: Database management/access gem. Supports adapters, migrations, and a
|
56
|
+
singleton to make SQL queries.
|
57
|
+
email: nate@ivytap.com
|
58
|
+
executables:
|
59
|
+
- mystic
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- LICENSE
|
64
|
+
- bin/mystic
|
65
|
+
- lib/mystic.rb
|
66
|
+
- lib/mystic/adapter.rb
|
67
|
+
- lib/mystic/adapters/abstract.rb
|
68
|
+
- lib/mystic/adapters/mysql.rb
|
69
|
+
- lib/mystic/adapters/postgres.rb
|
70
|
+
- lib/mystic/constants.rb
|
71
|
+
- lib/mystic/extensions.rb
|
72
|
+
- lib/mystic/migration.rb
|
73
|
+
- lib/mystic/model.rb
|
74
|
+
- lib/mystic/sql.rb
|
75
|
+
homepage: https://github.com/ivytap/mystic
|
76
|
+
licenses:
|
77
|
+
- MIT
|
78
|
+
metadata: {}
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 2.2.2
|
96
|
+
signing_key:
|
97
|
+
specification_version: 4
|
98
|
+
summary: Lightweight migrations + SQL execution
|
99
|
+
test_files: []
|