micro_sql 0.2.0 → 0.3.0

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.
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