mystic 0.0.1

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