micro_sql 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -3,12 +3,15 @@ source "http://rubygems.org"
3
3
  # Example:
4
4
  # gem "activesupport", ">= 2.3.5"
5
5
 
6
+ gem "pg"
7
+ gem "sqlite3"
8
+
6
9
  # Add dependencies to develop your gem here.
7
10
  # Include everything needed to run rake, tests, features, etc.
8
11
  group :development do
9
- gem "shoulda", ">= 0"
10
- gem "rdoc", "~> 3.12"
11
12
  gem "bundler"
12
13
  gem "jeweler"
13
- # gem "rcov", ">= 0"
14
+ gem 'simplecov', :require => false
15
+ gem "ruby-debug19"
14
16
  end
17
+
data/Gemfile.lock CHANGED
@@ -1,6 +1,8 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
+ archive-tar-minitar (0.5.2)
5
+ columnize (0.3.6)
4
6
  git (1.2.5)
5
7
  jeweler (1.8.3)
6
8
  bundler (~> 1.0)
@@ -8,14 +10,28 @@ GEM
8
10
  rake
9
11
  rdoc
10
12
  json (1.6.6)
13
+ linecache19 (0.5.12)
14
+ ruby_core_source (>= 0.1.4)
15
+ multi_json (1.2.0)
16
+ pg (0.13.2)
11
17
  rake (0.9.2.2)
12
18
  rdoc (3.12)
13
19
  json (~> 1.4)
14
- shoulda (3.0.1)
15
- shoulda-context (~> 1.0.0)
16
- shoulda-matchers (~> 1.0.0)
17
- shoulda-context (1.0.0)
18
- shoulda-matchers (1.0.0)
20
+ ruby-debug-base19 (0.11.25)
21
+ columnize (>= 0.3.1)
22
+ linecache19 (>= 0.5.11)
23
+ ruby_core_source (>= 0.1.4)
24
+ ruby-debug19 (0.11.6)
25
+ columnize (>= 0.3.1)
26
+ linecache19 (>= 0.5.11)
27
+ ruby-debug-base19 (>= 0.11.19)
28
+ ruby_core_source (0.1.5)
29
+ archive-tar-minitar (>= 0.5.2)
30
+ simplecov (0.6.1)
31
+ multi_json (~> 1.0)
32
+ simplecov-html (~> 0.5.3)
33
+ simplecov-html (0.5.3)
34
+ sqlite3 (1.3.5)
19
35
 
20
36
  PLATFORMS
21
37
  ruby
@@ -23,5 +39,7 @@ PLATFORMS
23
39
  DEPENDENCIES
24
40
  bundler
25
41
  jeweler
26
- rdoc (~> 3.12)
27
- shoulda
42
+ pg
43
+ ruby-debug19
44
+ simplecov
45
+ sqlite3
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.3.0
data/lib/micro_sql.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "uri"
2
+ require "logger"
2
3
 
3
4
  class MicroSql
4
5
  end
@@ -6,9 +7,16 @@ end
6
7
  require_relative "micro_sql/pg_adapter"
7
8
  require_relative "micro_sql/sqlite_adapter"
8
9
  require_relative "micro_sql/table"
9
- require_relative "micro_sql/settings"
10
+ require_relative "micro_sql/key_value_table"
10
11
 
11
12
  class MicroSql
13
+ class Error < RuntimeError; end
14
+
15
+ (class << self; self; end).class_eval do
16
+ attr :logger, true
17
+ end
18
+
19
+ self.logger = Logger.new(STDERR)
12
20
 
13
21
  (class << self; self; end).class_eval do
14
22
  private :new
@@ -26,15 +34,10 @@ class MicroSql
26
34
  end
27
35
 
28
36
  def ask(sql, *args)
29
- r = exec(sql, *args)
30
- return r unless r.is_a?(Array)
31
-
32
- r = r.first
33
- return r unless r.is_a?(Array)
34
- return r unless r.length == 1
35
- r.first
37
+ results = execute :ask, sql, *args
38
+ format_results_for_ask(results)
36
39
  end
37
-
40
+
38
41
  def exec!(sql, *args)
39
42
  execute :no_prepare, sql, *args
40
43
  end
@@ -43,27 +46,122 @@ class MicroSql
43
46
  execute :prepare, sql, *args
44
47
  end
45
48
 
49
+ private
50
+
51
+ def format_results_for_ask(results)
52
+ return results unless results.is_a?(Array)
53
+
54
+ results = results.first
55
+
56
+ return results unless results.is_a?(Array)
57
+ return results if results.length != 1
58
+
59
+ results.first
60
+ end
61
+
62
+ public
63
+
46
64
  def transaction(&block)
47
- @impl.transaction(&block)
65
+ r = nil
66
+ @impl.transaction do
67
+ r = yield
68
+ end
69
+ r
70
+ rescue RollbackException
71
+ nil
72
+ end
73
+
74
+ def rollback!
75
+ raise RollbackException
76
+ end
77
+
78
+ public
79
+
80
+ def insert(table, *values)
81
+ hashes, arrays = values.partition do |value| value.is_a?(Hash) end
82
+
83
+ id1 = insert_array_values(table, arrays)
84
+ id2 = insert_hash_values(table, hashes)
85
+
86
+ id1 || id2
87
+ end
88
+
89
+ private
90
+
91
+ def insert_array_values(table, values)
92
+ width = values.map(&:length).max
93
+ return if width == 0 || width == nil
94
+
95
+ sql = insert_sql(table, width, nil)
96
+ values.inject(0) do |r, value|
97
+ exec sql, *value
98
+ end
48
99
  end
49
100
 
50
- def settings
51
- @settings ||= MicroSql::SettingsTable.new(self)
101
+ def insert_hash_values(table, values)
102
+ keys = values.map(&:keys).flatten.uniq
103
+ return if keys.empty?
104
+
105
+ sql = insert_sql(table, keys.length, keys)
106
+ values.inject(0) do |r, rec|
107
+ exec sql, *rec.values_at(*keys)
108
+ end
52
109
  end
53
110
 
111
+ def insert_sql(table, width, keys = nil)
112
+ sql = "INSERT INTO #{table}"
113
+ sql += "(#{keys.join(",")})" if keys
114
+ sql += " VALUES(" + ([ "?" ] * width).join(",") + ")"
115
+ end
116
+
117
+ public
118
+
119
+ def tables
120
+ raise "Implementation missing: #{self.class}#tables"
121
+ end
122
+
123
+ def table(sql_or_name)
124
+ @tables ||= {}
125
+
126
+ return table_from_name(sql_or_name) if sql_or_name !~ /\s/
127
+
128
+ table = Table.new(self, sql_or_name)
129
+ @tables[table.table_name] = table
130
+ end
131
+
54
132
  private
55
133
 
134
+ def table_from_name(name)
135
+ @tables[name] ||= Table.new(self, name)
136
+ rescue RuntimeError
137
+ end
138
+
139
+ public
140
+
141
+ def key_value_table(name)
142
+ @key_value_tables ||= Hash.new { |hash, key|
143
+ hash[key] = MicroSql::KeyValueTable.new(self, key)
144
+ }
145
+
146
+ @key_value_tables[name]
147
+ end
148
+
149
+ private
150
+
151
+ class RollbackException < RuntimeError; end
152
+
56
153
  def prepared_queries
57
154
  @prepared_queries ||= {}
58
155
  end
59
156
 
157
+ def unprepare(key)
158
+ @prepared_queries.delete key
159
+ end
160
+
60
161
  def prepare(sql)
61
- key = sql.to_uid.to_s
62
- prepared_queries.fetch(key)
162
+ key = sql
163
+ prepared_queries.fetch(key)
63
164
  rescue KeyError
64
165
  prepared_queries[key] = prepare_query(key, sql)
65
166
  end
66
167
  end
67
-
68
- # url = "pg://sqdb:sqdb@localhost:5433/sqdb"
69
- # MicroSql.create(url)
@@ -0,0 +1,46 @@
1
+ # -- The settings table; has support for ttl etc. ---------------------------
2
+
3
+ require "json"
4
+
5
+ class MicroSql::KeyValueTable < MicroSql::Table
6
+ def initialize(db, table_name = "settings")
7
+ super db, "CREATE TABLE #{table_name}(uid TEXT PRIMARY KEY, value TEXT, ttl BIGINT)"
8
+ end
9
+
10
+ def [](key)
11
+ value, ttl = db.ask("SELECT value, ttl FROM #{table_name} WHERE uid=?", key)
12
+ decode(value) if !ttl || ttl < Time.now.to_i
13
+ end
14
+
15
+ def cached(key, ttl = nil, &block)
16
+ self[key] || update(key, yield, ttl)
17
+ end
18
+
19
+ def update(key, value, ttl = nil)
20
+ if value
21
+ encoded = encode(value)
22
+ ttl += Time.now.to_i if ttl
23
+ affected = @db.ask("UPDATE #{table_name} SET value=?, ttl=? WHERE uid=?", encoded, ttl, key)
24
+ if affected == 0
25
+ @db.ask("INSERT INTO #{table_name}(value, ttl, uid) VALUES(?,?,?)", encoded, ttl, key)
26
+ end
27
+ else
28
+ @db.ask("DELETE FROM #{table_name} WHERE uid=?", key)
29
+ end
30
+
31
+ value
32
+ end
33
+
34
+ alias :[]= :update
35
+
36
+ private
37
+
38
+ def decode(io)
39
+ return unless io
40
+ JSON.parse("[#{io}]").first
41
+ end
42
+
43
+ def encode(value)
44
+ value && value.to_json
45
+ end
46
+ end
@@ -9,57 +9,146 @@ class MicroSql::PgAdapter < MicroSql
9
9
  :user => uri.user,
10
10
  :password => uri.password,
11
11
  :dbname => uri.path[1..-1]
12
+
13
+ @impl.set_notice_receiver { |result| MicroSql.logger.info(result.error_message) }
12
14
  end
13
15
 
14
16
  def tables
15
17
  exec("SELECT tablename FROM pg_tables WHERE tablename NOT LIKE 'pg_%' AND tablename NOT LIKE 'sql_%'").map(&:first)
16
18
  end
17
19
 
20
+ def primary_keys(table)
21
+ sql = <<-SQL
22
+ SELECT pg_attribute.attname
23
+ FROM pg_index, pg_class, pg_attribute
24
+ WHERE
25
+ pg_class.oid = ?::regclass AND
26
+ indrelid = pg_class.oid AND
27
+ pg_attribute.attrelid = pg_class.oid AND
28
+ pg_attribute.attnum = any(pg_index.indkey) AND
29
+ indisprimary
30
+ SQL
31
+
32
+ exec(sql, table).map(&:first)
33
+ end
34
+
35
+ def primary_key(table)
36
+ keys = primary_keys(table)
37
+ raise(Error, "No support for primary key (in table #{table})") if keys.length > 1
38
+ keys.first || raise(Error, "No primary key in table #{table}")
39
+ end
40
+
41
+ private
42
+
43
+ def insert_sql(table_name, *args)
44
+ "#{super} RETURNING #{table(table_name).primary_key}"
45
+ end
46
+
47
+ public
48
+
18
49
  def execute_batch(sql)
19
- sql.split(";").each { exec(part) }
50
+ sql.split(";").each { |part|
51
+ exec!(part)
52
+ }
20
53
  end
21
54
 
22
55
  private
23
56
 
24
57
  def execute(flag, sql, *args)
25
- statement = prepare(sql) if flag == :prepare
26
- result = if statement
27
- @impl.exec_prepared(statement, args)
58
+ execute_(flag, sql, *args)
59
+ rescue PG::Error
60
+ raise Error, $!.message
61
+ end
62
+
63
+ def execute_(flag, sql, *args)
64
+ adjusted_sql = replace_placeholders(sql)
65
+
66
+ result = if adjusted_sql && (flag == :prepare || flag == :ask) && prepared_statement = prepare(adjusted_sql)
67
+ @impl.exec_prepared(prepared_statement, args)
28
68
  else
29
- @impl.exec(sql, args)
69
+ @impl.exec(adjusted_sql || sql, args)
30
70
  end
31
71
 
32
- if sql =~ /^\s*(UPDATE|DELETE|INSERT)/i
72
+ case sql
73
+ when /^\s*UPDATE\b/
33
74
  return result.cmd_tuples
75
+ when /^\s*DELETE\b/
76
+ return result.cmd_tuples
77
+ when /^\s*INSERT\b/
78
+ # postgresql has a different way of returning newly inserted ids; e.g.
79
+ # "INSERT ... RETURNING id"
80
+ # The returned id value is returned to the pg driver the same as
81
+ # a SELECT would be. We therefore evaluate only the first record.
82
+ flag = :insert
83
+ records = result.values
84
+ else
85
+ records = result.values
86
+ records = records[0,1] if flag == :ask
34
87
  end
35
88
 
36
- types = (0 ... result.num_fields).map do |i|
37
- # Get the type of the second column of the result 'res'
38
- @impl.exec( "SELECT format_type($1,$2)", [result.ftype(i), result.fmod(i)] ).getvalue( 0, 0 )
89
+ result = convert_records result, records
90
+
91
+ flag == :insert ? format_results_for_ask(result) : result
92
+ end
93
+
94
+ module Conversion
95
+ extend self
96
+
97
+ def nop; end
98
+
99
+ def string(s); s; end
100
+ def unknown(s); s; end
101
+ def integer(s); s.to_i; end
102
+ end
103
+
104
+ def conversion_for(ftype, fmod)
105
+ sym = case ftype
106
+ when 16 then :boolean
107
+ when 17 then :bytea
108
+ when 19 then :nop
109
+ when 20, 21, 22, 23, 26 then :integer
110
+ when 25 then :nop # "text"
111
+ when 700, 701 then :float
112
+ when 790, 1700 then :big_decimal
113
+ when 1083, 1266 then :string_to_time
39
114
  end
115
+
116
+ return sym if sym && Conversion.respond_to?(sym)
117
+ raise "Unsupported conversion #{sym.inspect}" if sym
118
+
119
+ typename = @impl.exec( "SELECT format_type($1,$2)", [ftype, fmod] ).getvalue( 0, 0 )
120
+
121
+ return :nop if typename == "unknown"
122
+ raise "Unsupported format_type #{typename.inspect} (ftype, fmod: #{ftype}, #{fmod})"
123
+ end
124
+
125
+ def convert_records(result, records)
126
+ # Get the type of the result columns
127
+ converters = (0 ... result.num_fields).map do |i|
128
+ conversion = conversion_for result.ftype(i), result.fmod(i)
129
+ [ conversion, i ] unless conversion == :nop
130
+ end.compact
40
131
 
41
- result.values.map do |rec|
42
- types.each_with_index do |type, idx|
43
- case type
44
- when "text", "name" then :nop
45
- when "integer", "bigint" then rec[idx] = rec[idx] && rec[idx].to_i
46
- else raise "Unknown column type #{type}: #{rec[idx].inspect}"
47
- end
132
+ records.map do |record|
133
+ converters.each do |type, idx|
134
+ record[idx] = Conversion.send type, record[idx]
48
135
  end
49
-
50
- rec
136
+ record
51
137
  end
52
138
  end
53
-
54
- def prepare_query(key, sql)
139
+
140
+ def replace_placeholders(sql)
55
141
  idx = 0
56
142
  query = sql.gsub("?") do
57
143
  idx += 1
58
144
  "$#{idx}"
59
145
  end
60
-
61
- return nil if idx == 0
62
- @impl.prepare(key, query)
146
+
147
+ idx == 0 ? nil : query
148
+ end
149
+
150
+ def prepare_query(key, sql)
151
+ @impl.prepare(key, sql)
63
152
  key
64
153
  end
65
154
  end
@@ -4,7 +4,7 @@ class MicroSql::SqliteAdapter < MicroSql
4
4
  def initialize(url)
5
5
  uri = URI.parse(url)
6
6
  @impl = SQLite3::Database.new(uri.path)
7
- exec "PRAGMA synchronous = OFF"
7
+ exec! "PRAGMA synchronous = OFF"
8
8
  end
9
9
 
10
10
  def execute_batch(sql)
@@ -15,17 +15,27 @@ class MicroSql::SqliteAdapter < MicroSql
15
15
  exec("SELECT name FROM sqlite_master WHERE type=?", "table").map(&:first)
16
16
  end
17
17
 
18
- def exec(sql, *args)
19
- results = prepare(sql).execute!(*args)
18
+ private
19
+
20
+ def execute(prepare, sql, *args)
21
+ prepared_query = prepare(sql)
22
+ execute_prepared_query sql, prepared_query, *args
23
+ ensure
24
+ unprepare(sql) if prepare == :no_prepare
25
+ end
26
+
27
+ def execute_prepared_query(sql, query, *args)
28
+ results = query.execute!(*args)
20
29
  case sql
21
- when /^\s*INSERT/i then @impl.last_insert_row_id
22
- when /^\s*(UPDATE|DELETE)/i then @impl.changes
23
- else results
30
+ when /^\s*INSERT/i then @impl.last_insert_row_id
31
+ when /^\s*UPDATE/i then @impl.changes
32
+ when /^\s*DELETE/i then @impl.changes
33
+ else results
24
34
  end
35
+ rescue SQLite3::ConstraintException
36
+ raise Error, $!.message
25
37
  end
26
38
 
27
- private
28
-
29
39
  def prepare_query(key, sql)
30
40
  @impl.prepare(sql)
31
41
  end
@@ -5,15 +5,26 @@ class MicroSql::Table
5
5
 
6
6
  def initialize(db, sql)
7
7
  @db = db
8
- if sql =~ /\s*CREATE TABLE\s+([^\( ]+)/
8
+
9
+ if sql !~ /\s/
10
+ # no space: sql must be the table_name
11
+ @table_name = sql
12
+ raise "No such table: '#{@table_name}'" unless exists?
13
+ elsif sql =~ /\s*CREATE TABLE\s+([^\( ]+)/
9
14
  @table_name = $1
15
+ raise ArgumentError, "Cannot determine table_name from SQL: #{sql}" unless table_name
16
+ build(sql) unless exists?
10
17
  end
11
-
12
- raise ArgumentError, "Cannot determine table_name from SQL: #{sql}" unless table_name
13
-
14
- build(sql) unless exists?
15
18
  end
16
19
 
20
+ def insert(*values)
21
+ db.insert table_name, *values
22
+ end
23
+
24
+ def primary_key
25
+ @primary_key ||= db.primary_key(table_name)
26
+ end
27
+
17
28
  private
18
29
 
19
30
  def exists?
@@ -21,7 +32,7 @@ class MicroSql::Table
21
32
  end
22
33
 
23
34
  def build(sql)
24
- LOGGER.warn "Create table #{table_name}"
35
+ MicroSql.logger.info "Create table #{table_name}"
25
36
  db.execute_batch(sql)
26
37
  end
27
38
  end
data/micro_sql.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "micro_sql"
8
- s.version = "0.2.0"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["radiospiel"]
12
- s.date = "2012-03-30"
12
+ s.date = "2012-04-02"
13
13
  s.description = "You only need a single method to talk to your database..."
14
14
  s.email = "eno@open-lab.org"
15
15
  s.extra_rdoc_files = [
@@ -25,13 +25,16 @@ Gem::Specification.new do |s|
25
25
  "Rakefile",
26
26
  "VERSION",
27
27
  "lib/micro_sql.rb",
28
+ "lib/micro_sql/key_value_table.rb",
28
29
  "lib/micro_sql/pg_adapter.rb",
29
- "lib/micro_sql/settings.rb",
30
30
  "lib/micro_sql/sqlite_adapter.rb",
31
31
  "lib/micro_sql/table.rb",
32
32
  "micro_sql.gemspec",
33
+ "script/watchr",
33
34
  "test/helper.rb",
34
- "test/test_micro_sql.rb"
35
+ "test/test_micro_sql.rb",
36
+ "test/test_micro_sql_pg.rb",
37
+ "test/test_micro_sql_sqlite3.rb"
35
38
  ]
36
39
  s.homepage = "http://github.com/radiospiel/micro_sql"
37
40
  s.licenses = ["MIT"]
@@ -43,21 +46,27 @@ Gem::Specification.new do |s|
43
46
  s.specification_version = 3
44
47
 
45
48
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
46
- s.add_development_dependency(%q<shoulda>, [">= 0"])
47
- s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
49
+ s.add_runtime_dependency(%q<pg>, [">= 0"])
50
+ s.add_runtime_dependency(%q<sqlite3>, [">= 0"])
48
51
  s.add_development_dependency(%q<bundler>, [">= 0"])
49
52
  s.add_development_dependency(%q<jeweler>, [">= 0"])
53
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
54
+ s.add_development_dependency(%q<ruby-debug19>, [">= 0"])
50
55
  else
51
- s.add_dependency(%q<shoulda>, [">= 0"])
52
- s.add_dependency(%q<rdoc>, ["~> 3.12"])
56
+ s.add_dependency(%q<pg>, [">= 0"])
57
+ s.add_dependency(%q<sqlite3>, [">= 0"])
53
58
  s.add_dependency(%q<bundler>, [">= 0"])
54
59
  s.add_dependency(%q<jeweler>, [">= 0"])
60
+ s.add_dependency(%q<simplecov>, [">= 0"])
61
+ s.add_dependency(%q<ruby-debug19>, [">= 0"])
55
62
  end
56
63
  else
57
- s.add_dependency(%q<shoulda>, [">= 0"])
58
- s.add_dependency(%q<rdoc>, ["~> 3.12"])
64
+ s.add_dependency(%q<pg>, [">= 0"])
65
+ s.add_dependency(%q<sqlite3>, [">= 0"])
59
66
  s.add_dependency(%q<bundler>, [">= 0"])
60
67
  s.add_dependency(%q<jeweler>, [">= 0"])
68
+ s.add_dependency(%q<simplecov>, [">= 0"])
69
+ s.add_dependency(%q<ruby-debug19>, [">= 0"])
61
70
  end
62
71
  end
63
72
 
data/script/watchr ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rb-fsevent'
3
+
4
+ if ARGV.length < 2
5
+ STDERR.puts "watchr dir ruby-parameter(s)..."
6
+ abort
7
+ end
8
+
9
+ paths = ARGV.shift.split(",")
10
+ $args = ARGV.dup
11
+
12
+
13
+ def do_ruby
14
+ STDERR.puts $args.join(" ")
15
+ STDERR.puts "=" * 80
16
+
17
+ system(*$args)
18
+ end
19
+
20
+ puts "Initial run"
21
+ do_ruby
22
+
23
+ fsevent = FSEvent.new
24
+ fsevent.watch paths do |directories|
25
+ puts "Detected change inside: #{directories.inspect}"
26
+ do_ruby
27
+ end
28
+ fsevent.run
data/test/helper.rb CHANGED
@@ -7,12 +7,20 @@ rescue Bundler::BundlerError => e
7
7
  $stderr.puts "Run `bundle install` to install missing gems"
8
8
  exit e.status_code
9
9
  end
10
+
11
+ require 'ruby-debug'
12
+ require 'simplecov'
10
13
  require 'test/unit'
11
- require 'shoulda'
14
+ SimpleCov.start do
15
+ add_filter "test/helper.rb"
16
+ end
12
17
 
13
18
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
19
  $LOAD_PATH.unshift(File.dirname(__FILE__))
15
20
  require 'micro_sql'
16
21
 
22
+ MicroSql.logger.level = Logger::WARN
23
+
17
24
  class Test::Unit::TestCase
18
25
  end
26
+
@@ -1,7 +1,9 @@
1
1
  require 'helper'
2
2
 
3
3
  class TestMicroSql < Test::Unit::TestCase
4
- should "probably rename this file and start testing for real" do
5
- flunk "hey buddy, you should probably rename this file and start testing for real"
4
+ def test_assert_creation_with_unsupported_url
5
+ assert_raise(NameError) {
6
+ MicroSql.create "xy://abc"
7
+ }
6
8
  end
7
9
  end
@@ -0,0 +1,139 @@
1
+ load "#{File.dirname(__FILE__)}/helper.rb"
2
+
3
+ class TestMicroSqlPg < Test::Unit::TestCase
4
+ PG_URL = "pg://micro_sql:micro_sql@localhost/micro_sql_test"
5
+
6
+ def db
7
+ @db ||= MicroSql.create(PG_URL).tap do |db|
8
+ db.tables.each { |table_name| db.exec("DROP TABLE #{table_name}") }
9
+ end
10
+ end
11
+
12
+ def test_assert_create_tables
13
+ db.execute_batch <<-SQL
14
+ CREATE TABLE t1(idx INTEGER PRIMARY KEY, b TEXT);
15
+ CREATE TABLE t2(a INTEGER, b TEXT);
16
+ SQL
17
+
18
+ assert_equal %w(t1 t2), db.tables.sort
19
+ end
20
+
21
+ def test_ask_and_exec
22
+ db.execute_batch <<-SQL
23
+ DROP SEQUENCE IF EXISTS t1_idx_seq;
24
+ CREATE SEQUENCE t1_idx_seq;
25
+ CREATE TABLE t1(idx INTEGER PRIMARY KEY NOT NULL DEFAULT nextval('t1_idx_seq'), b TEXT);
26
+ ALTER SEQUENCE t1_idx_seq OWNED BY t1.idx
27
+ SQL
28
+
29
+ # -- insert
30
+ r = db.ask("INSERT INTO t1 (idx,b) VALUES(12, '') RETURNING idx")
31
+ assert_equal 12, r
32
+ assert_equal 13, db.exec("INSERT INTO t1 (idx,b) VALUES(13, '') RETURNING idx")
33
+
34
+ assert_raise(MicroSql::Error) {
35
+ assert_equal 1, db.exec("INSERT INTO t1 (idx,b) VALUES(13, '') RETURNING idx")
36
+ }
37
+ assert_raise(MicroSql::Error) {
38
+ assert_equal 1, db.ask("INSERT INTO t1 (idx,b) VALUES(13, '') RETURNING idx")
39
+ }
40
+
41
+ # -- update
42
+
43
+ assert_equal 1, db.ask("UPDATE t1 SET b = 'b' WHERE idx=12")
44
+ assert_equal 2, db.ask("UPDATE t1 SET b = 'c'")
45
+ assert_equal 0, db.ask("UPDATE t1 SET b = 'b' WHERE idx=12345")
46
+
47
+ # assert_equal 1, db.exec("INSERT INTO t1 (idx,b) VALUES(13, '')")
48
+ # assert_equal(13, r)
49
+ #
50
+
51
+ # -- delete
52
+ assert_equal 1, db.ask("DELETE FROM t1 WHERE idx=12")
53
+ assert_equal 0, db.exec("DELETE FROM t1 WHERE idx=12")
54
+ assert_equal 1, db.exec("DELETE FROM t1 WHERE idx=13")
55
+ end
56
+
57
+ def test_type_conversion
58
+ assert_equal(1, db.ask("SELECT 1"))
59
+ assert_equal('1', db.ask("SELECT '1'"))
60
+ end
61
+
62
+ def test_row_conversion
63
+ assert_equal(1, db.ask("SELECT 1"))
64
+ assert_equal([1, 2], db.ask("SELECT 1, 2"))
65
+ end
66
+
67
+ def test_arguments
68
+ db.ask "CREATE TABLE t1(idx INTEGER PRIMARY KEY, b TEXT)"
69
+ assert_equal nil, db.ask("SELECT idx FROM t1 WHERE idx=?", 12)
70
+
71
+ db.ask("INSERT INTO t1 (idx,b) VALUES(12, 'twelve')")
72
+ db.ask("INSERT INTO t1 (idx,b) VALUES(13, 'thirteen')")
73
+ db.ask("INSERT INTO t1 (idx,b) VALUES(0, NULL)")
74
+
75
+ assert_equal 3, db.ask("SELECT COUNT(*) FROM t1")
76
+ assert_equal 12, db.ask("SELECT idx FROM t1 WHERE idx=12")
77
+ assert_equal 12, db.ask("SELECT idx FROM t1 WHERE idx=?", 12)
78
+ assert_equal 12, db.ask("SELECT idx FROM t1 WHERE b=?", "twelve")
79
+ assert_equal 12, db.ask("SELECT idx FROM t1 WHERE b=? AND idx=?", "twelve", 12)
80
+ end
81
+
82
+ def test_transaction
83
+ db.ask "CREATE TABLE t1(idx INTEGER PRIMARY KEY, b TEXT)"
84
+
85
+ count = db.transaction do
86
+ db.ask("INSERT INTO t1 (idx,b) VALUES(12, 'twelve')")
87
+ db.ask("SELECT COUNT(*) FROM t1")
88
+ end
89
+ assert_equal 1, count
90
+
91
+ assert_equal 1, db.ask("SELECT COUNT(*) FROM t1")
92
+
93
+ db.transaction do
94
+ db.ask("INSERT INTO t1 (idx,b) VALUES(13, 'twelve')")
95
+ assert_equal 2, db.ask("SELECT COUNT(*) FROM t1")
96
+ db.rollback!
97
+ end
98
+
99
+ assert_equal 1, db.ask("SELECT COUNT(*) FROM t1")
100
+ end
101
+
102
+ def test_insert
103
+ assert_equal([], db.tables)
104
+ t1 = db.table "CREATE TABLE t1(idx INTEGER PRIMARY KEY, b TEXT)"
105
+
106
+ assert_equal(["t1"], db.tables)
107
+ assert_equal("t1", t1.table_name)
108
+
109
+ assert_equal nil, db.table("t2")
110
+
111
+ t1.insert [1, "1"], [2, "2"]
112
+ assert_equal [[1], [2]], db.exec("SELECT idx FROM t1")
113
+
114
+ assert_equal(3, t1.insert([3, "3"]))
115
+
116
+ assert_equal(4, t1.insert(:idx => 4))
117
+ end
118
+
119
+ def test_kv_table
120
+ kv = db.key_value_table("kv")
121
+ assert_equal [ "kv" ], db.tables
122
+
123
+ kv["x"] = "y"
124
+ assert_equal 1, db.ask("SELECT COUNT(*) FROM kv")
125
+ assert_equal "y", kv["x"]
126
+
127
+ kv["x"] = nil
128
+ assert_equal 0, db.ask("SELECT COUNT(*) FROM kv")
129
+ assert_equal nil, kv["x"]
130
+
131
+ count = 0
132
+
133
+ r = kv.cached "x" do "y" end
134
+ assert_equal(r, "y")
135
+
136
+ r = kv.cached "x" do raise "Yo"; "y" end
137
+ assert_equal(r, "y")
138
+ end
139
+ end
@@ -0,0 +1,173 @@
1
+ require 'helper'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+
5
+ class TestMicroSqlite3 < Test::Unit::TestCase
6
+ attr :tmpdir
7
+
8
+ def setup
9
+ @tmpdir = Dir.mktmpdir
10
+ end
11
+
12
+ def teardown
13
+ FileUtils.rm_rf(@tmpdir) if @tmpdir
14
+ end
15
+
16
+ def db
17
+ @db ||= MicroSql.create "#{tmpdir}/test.sqlite3"
18
+ end
19
+
20
+ def test_assert_creation_of_db_file_from_url
21
+ path = "#{tmpdir}/db.sqlite3"
22
+ assert !File.exists?(path)
23
+
24
+ url = "sqlite://#{path}"
25
+ db = MicroSql.create url
26
+ assert File.exists?(path)
27
+ end
28
+
29
+ def test_assert_creation_of_db_file_from_path
30
+ path = "#{tmpdir}/db.sqlite3"
31
+ assert !File.exists?(path)
32
+
33
+ db = MicroSql.create path
34
+ assert File.exists?(path)
35
+ end
36
+
37
+ def test_assert_create_tables
38
+ db.execute_batch <<-SQL
39
+ CREATE TABLE t1(idx INTEGER PRIMARY KEY, b INTEGER);
40
+ CREATE TABLE t2(a INTEGER, b INTEGER);
41
+ SQL
42
+
43
+ assert_equal %w(t1 t2), db.tables.sort
44
+ end
45
+
46
+ def test_ask_and_exec
47
+ db.ask "CREATE TABLE t1(idx INTEGER PRIMARY KEY, b INTEGER)"
48
+
49
+ # -- insert
50
+ assert_equal 12, db.ask("INSERT INTO t1 (idx,b) VALUES(12, '')")
51
+ assert_equal 13, db.exec("INSERT INTO t1 (idx,b) VALUES(13, '')")
52
+
53
+ assert_raise(MicroSql::Error) {
54
+ assert_equal 1, db.exec("INSERT INTO t1 (idx,b) VALUES(13, '')")
55
+ }
56
+ assert_raise(MicroSql::Error) {
57
+ assert_equal 1, db.ask("INSERT INTO t1 (idx,b) VALUES(13, '')")
58
+ }
59
+
60
+ # -- update
61
+
62
+ assert_equal 1, db.ask("UPDATE t1 SET b = 'b' WHERE idx=12")
63
+ assert_equal 2, db.ask("UPDATE t1 SET b = 'c'")
64
+ assert_equal 0, db.ask("UPDATE t1 SET b = 'b' WHERE idx=12345")
65
+
66
+ # assert_equal 1, db.exec("INSERT INTO t1 (idx,b) VALUES(13, '')")
67
+ # assert_equal(13, r)
68
+ #
69
+
70
+ # -- delete
71
+ assert_equal 1, db.ask("DELETE FROM t1 WHERE idx=12")
72
+ assert_equal 0, db.exec("DELETE FROM t1 WHERE idx=12")
73
+ assert_equal 1, db.exec("DELETE FROM t1 WHERE idx=13")
74
+ end
75
+
76
+ def test_preparation
77
+ assert_equal(0, db.send(:prepared_queries).length)
78
+
79
+ db.exec("SELECT 1")
80
+ assert_equal(1, db.send(:prepared_queries).length)
81
+
82
+ db.ask("SELECT 2")
83
+ assert_equal(2, db.send(:prepared_queries).length)
84
+
85
+ db.ask("SELECT 2")
86
+ assert_equal(2, db.send(:prepared_queries).length)
87
+
88
+ db.exec!("SELECT 3")
89
+ assert_equal(2, db.send(:prepared_queries).length)
90
+ end
91
+
92
+ def test_type_conversion
93
+ assert_equal(1, db.ask("SELECT 1"))
94
+ assert_equal('1', db.ask("SELECT '1'"))
95
+ end
96
+
97
+ def test_row_conversion
98
+ assert_equal(1, db.ask("SELECT 1"))
99
+ assert_equal([1, 2], db.ask("SELECT 1, 2"))
100
+ end
101
+
102
+ def test_arguments
103
+ db.ask "CREATE TABLE t1(idx INTEGER PRIMARY KEY, b INTEGER)"
104
+ db.ask("INSERT INTO t1 (idx,b) VALUES(12, 'twelve')")
105
+ db.ask("INSERT INTO t1 (idx,b) VALUES(13, 'thirteen')")
106
+ db.ask("INSERT INTO t1 (idx,b) VALUES(0, NULL)")
107
+
108
+ assert_equal 3, db.ask("SELECT COUNT(*) FROM t1")
109
+ assert_equal 12, db.ask("SELECT idx FROM t1 WHERE idx=?", 12)
110
+ assert_equal 12, db.ask("SELECT idx FROM t1 WHERE b=?", "twelve")
111
+ assert_equal 12, db.ask("SELECT idx FROM t1 WHERE b=? AND idx=?", "twelve", 12)
112
+ assert_equal 0, db.ask("SELECT idx FROM t1 WHERE b IS ?", nil)
113
+ assert_equal nil, db.ask("SELECT idx FROM t1 WHERE b=?", nil) # b=NULL is never true but always NULL
114
+ end
115
+
116
+ def test_transaction
117
+ db.ask "CREATE TABLE t1(idx INTEGER PRIMARY KEY, b INTEGER)"
118
+
119
+ count = db.transaction do
120
+ db.ask("INSERT INTO t1 (idx,b) VALUES(12, 'twelve')")
121
+ db.ask("SELECT COUNT(*) FROM t1")
122
+ end
123
+ assert_equal 1, count
124
+
125
+ assert_equal 1, db.ask("SELECT COUNT(*) FROM t1")
126
+
127
+ db.transaction do
128
+ db.ask("INSERT INTO t1 (idx,b) VALUES(13, 'twelve')")
129
+ assert_equal 2, db.ask("SELECT COUNT(*) FROM t1")
130
+ db.rollback!
131
+ end
132
+
133
+ assert_equal 1, db.ask("SELECT COUNT(*) FROM t1")
134
+ end
135
+
136
+ def test_insert
137
+ assert_equal([], db.tables)
138
+ t1 = db.table "CREATE TABLE t1(idx INTEGER PRIMARY KEY, b TEXT)"
139
+
140
+ assert_equal(["t1"], db.tables)
141
+ assert_equal("t1", t1.table_name)
142
+
143
+ assert_equal nil, db.table("t2")
144
+
145
+ t1.insert [1, "1"], [2, "2"]
146
+ assert_equal [[1], [2]], db.exec("SELECT idx FROM t1")
147
+
148
+ assert_equal(3, t1.insert([3, "3"]))
149
+
150
+ assert_equal(4, t1.insert(:idx => 4))
151
+ end
152
+
153
+ def test_kv_table
154
+ kv = db.key_value_table("kv")
155
+ assert_equal [ "kv" ], db.tables
156
+
157
+ kv["x"] = "y"
158
+ assert_equal 1, db.ask("SELECT COUNT(*) FROM kv")
159
+ assert_equal "y", kv["x"]
160
+
161
+ kv["x"] = nil
162
+ assert_equal 0, db.ask("SELECT COUNT(*) FROM kv")
163
+ assert_equal nil, kv["x"]
164
+
165
+ count = 0
166
+
167
+ r = kv.cached "x" do "y" end
168
+ assert_equal(r, "y")
169
+
170
+ r = kv.cached "x" do raise "Yo"; "y" end
171
+ assert_equal(r, "y")
172
+ end
173
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: micro_sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,33 +9,33 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-30 00:00:00.000000000Z
12
+ date: 2012-04-02 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: shoulda
16
- requirement: &70301986679420 !ruby/object:Gem::Requirement
15
+ name: pg
16
+ requirement: &70130046726220 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
20
20
  - !ruby/object:Gem::Version
21
21
  version: '0'
22
- type: :development
22
+ type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70301986679420
24
+ version_requirements: *70130046726220
25
25
  - !ruby/object:Gem::Dependency
26
- name: rdoc
27
- requirement: &70301986678800 !ruby/object:Gem::Requirement
26
+ name: sqlite3
27
+ requirement: &70130046701360 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
- - - ~>
30
+ - - ! '>='
31
31
  - !ruby/object:Gem::Version
32
- version: '3.12'
33
- type: :development
32
+ version: '0'
33
+ type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70301986678800
35
+ version_requirements: *70130046701360
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: bundler
38
- requirement: &70301986678140 !ruby/object:Gem::Requirement
38
+ requirement: &70130046688900 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,32 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70301986678140
46
+ version_requirements: *70130046688900
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: jeweler
49
- requirement: &70301986677540 !ruby/object:Gem::Requirement
49
+ requirement: &70130046663760 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70130046663760
58
+ - !ruby/object:Gem::Dependency
59
+ name: simplecov
60
+ requirement: &70130046580920 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70130046580920
69
+ - !ruby/object:Gem::Dependency
70
+ name: ruby-debug19
71
+ requirement: &70130046570120 !ruby/object:Gem::Requirement
50
72
  none: false
51
73
  requirements:
52
74
  - - ! '>='
@@ -54,7 +76,7 @@ dependencies:
54
76
  version: '0'
55
77
  type: :development
56
78
  prerelease: false
57
- version_requirements: *70301986677540
79
+ version_requirements: *70130046570120
58
80
  description: You only need a single method to talk to your database...
59
81
  email: eno@open-lab.org
60
82
  executables: []
@@ -71,13 +93,16 @@ files:
71
93
  - Rakefile
72
94
  - VERSION
73
95
  - lib/micro_sql.rb
96
+ - lib/micro_sql/key_value_table.rb
74
97
  - lib/micro_sql/pg_adapter.rb
75
- - lib/micro_sql/settings.rb
76
98
  - lib/micro_sql/sqlite_adapter.rb
77
99
  - lib/micro_sql/table.rb
78
100
  - micro_sql.gemspec
101
+ - script/watchr
79
102
  - test/helper.rb
80
103
  - test/test_micro_sql.rb
104
+ - test/test_micro_sql_pg.rb
105
+ - test/test_micro_sql_sqlite3.rb
81
106
  homepage: http://github.com/radiospiel/micro_sql
82
107
  licenses:
83
108
  - MIT
@@ -93,7 +118,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
93
118
  version: '0'
94
119
  segments:
95
120
  - 0
96
- hash: 4072090889044300624
121
+ hash: -292595813074764874
97
122
  required_rubygems_version: !ruby/object:Gem::Requirement
98
123
  none: false
99
124
  requirements:
@@ -1,56 +0,0 @@
1
- # -- The settings table; has support for ttl etc. ---------------------------
2
-
3
- require "json"
4
-
5
- class MicroSql::SettingsTable < MicroSql::Table
6
- def initialize(db)
7
- super db, "CREATE TABLE settings(uid TEXT PRIMARY KEY, value TEXT, ttl BIGINT)"
8
- end
9
-
10
- def [](key)
11
- value, ttl = db.ask("SELECT value, ttl FROM settings WHERE uid=?", key)
12
- decode(value) if !ttl || ttl < Time.now.to_i
13
- end
14
-
15
- def cached(key, ttl, &block)
16
- update(key, yield, ttl)
17
- end
18
-
19
- def update(key, value, ttl = nil)
20
- if value
21
- encoded = encode(value)
22
- ttl += Time.now.to_i if ttl
23
- affected = @db.ask("UPDATE settings SET value=?, ttl=? WHERE uid=?", encoded, ttl, key)
24
- if affected == 0
25
- @db.ask("INSERT INTO settings(value, ttl, uid) VALUES(?,?,?)", encoded, ttl, key)
26
- end
27
- else
28
- @db.ask("DELETE FROM settings WHERE uid=?", key)
29
- end
30
-
31
- value
32
- end
33
-
34
- alias :[]= :update
35
-
36
- def decode(io)
37
- return unless io
38
- JSON.parse("[#{io}]").first
39
- end
40
-
41
- def encode(value)
42
- value && value.to_json
43
- end
44
- end
45
-
46
- require "forwardable"
47
-
48
- class MicroSql::Settings
49
- def initialize(url)
50
- @db = MicroSql.create(url)
51
- @settings_table = MicroSql::SettingsTable.new @db
52
- end
53
-
54
- extend Forwardable
55
- delegate [ :[], :cached, :update, :[]= ] => :@settings_table
56
- end