taupe 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f1b548f21b57a427e78515d3ea17ac0974f3a9d0
4
+ data.tar.gz: d25d1f8a2dd03b30b583e48f99a353fa09597528
5
+ SHA512:
6
+ metadata.gz: ef228a38b35977d1cada2add8000afeacf0d7756b42714d27d3b5806eb134193a0aa5f376aa56582440c8bd9451c7e52cc94c4b24b65593a01c5f764be248581
7
+ data.tar.gz: 560d21ddc307e63daa65469bd566b9c8062ec1d99aabf7ade937be744e70fa4bf199047aceef660d2364604a7843ee16e7ae814480639b1462c4b0c02b6bdc91
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ Metrics/ClassLength:
2
+ CountComments: false
3
+ Max: 150
4
+
5
+ Metrics/MethodLength:
6
+ CountComments: false
7
+ Max: 25
8
+
9
+ Style/Encoding:
10
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Utils
4
+ gem 'rake'
5
+ gem 'rspec'
6
+ gem 'yard'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Pierre Lecocq
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # Taupe
2
+
3
+ ## What is it?
4
+
5
+ **Taupe** is a simple and elegant database model manager.
6
+
7
+ It relies on several popular backends:
8
+
9
+ - *postgresql*, *mysql* or *sqlite* for database engines
10
+ - *memcached* or *redis* for cache engines
11
+
12
+ The main idea is to set a database connection, an optional cache connection and your models definitions.
13
+ That's it.
14
+
15
+ Then, you can query on the database connection or on your models directly. If a cache backend is defined, the data will be stored and retrieved faster than ever.
16
+
17
+ No fancy SQL DSL. Just pure SQL. Let's be braves and responsibles.
18
+
19
+ ## Why developping it?
20
+
21
+ It was developped for two main pros:
22
+
23
+ - control
24
+ - power
25
+
26
+ I can hear you: "But for this, there are some very popular ORMs like *ActiveRecord* or *Sequel*, you #@$!&+ ! "
27
+
28
+ Yes.
29
+ But No.
30
+ Definitely.
31
+
32
+ Here are the main reasons:
33
+
34
+ - ORMs are too complex for a large percent of our everyday use projects
35
+ - ORMs do not give you the opportunity to write SQL and control your queries (no control, no power). Well, yes they do, but for this, they load so many unused classes that your application's performances are really affected.
36
+ - ORMs have limits on joins, sub-queries, CTEs (... etc) and this is frustrating when doing hard SQL work like statistics
37
+
38
+ ## How to use it?
39
+
40
+ ### Prerequisites
41
+
42
+ In order to install the Taupe library, simply install the corresponding gem with
43
+
44
+ sudo gem install taupe
45
+
46
+ After that, you must have a few gems install on your system, depending on which backends you want to use
47
+
48
+ # If you use PostgreSQL database
49
+ sudo gem install pg
50
+
51
+ # If you use MySQL database
52
+ sudo gem install mysql2
53
+
54
+ # If you use Sqlite database
55
+ sudo gem install sqlite3
56
+
57
+ # If you use Memcached
58
+ sudo gem install memcached
59
+
60
+ # If you use Redis
61
+ sudo gem install redis
62
+
63
+ ### Define a SQL backend
64
+
65
+ #### Postgresql (highly recommended)
66
+
67
+ # Setup a postgresql database backend
68
+ Taupe::Database.setup do
69
+ type :postgresql
70
+ host :localhost
71
+ username :myuser
72
+ password :myfuckingstrongpassword
73
+ database :mydatabase
74
+ end
75
+
76
+ #### MySQL (not recommended)
77
+
78
+ # Setup a mysql database backend
79
+ Taupe::Database.setup do
80
+ type :mysql
81
+ host :localhost
82
+ username :myuser
83
+ password :myfuckingstrongpassword
84
+ database :mydatabase
85
+ end
86
+
87
+ #### SQLite
88
+
89
+ # Setup a sqlite database backend
90
+ Taupe::Database.setup do
91
+ type :sqlite
92
+ database File.expand_path('/tmp/mydatabase.db')
93
+ end
94
+
95
+ ### Optionally define a cache backend (recommended)
96
+
97
+ #### Memcached
98
+
99
+ # Setup a memcached cache backend
100
+ Taupe::Cache.setup do
101
+ type :memcached
102
+ host :localhost
103
+ port :11211
104
+ end
105
+
106
+ #### Redis
107
+
108
+ # Setup a redis cache backend
109
+ Taupe::Cache.setup do
110
+ type :redis
111
+ host :localhost
112
+ port :6379
113
+ end
114
+
115
+ ### Define some models
116
+
117
+ # Setup a model
118
+ Taupe::Model.setup do
119
+ table :article
120
+ column :article_id, { type: Integer, :primary_key => true }
121
+ column :title, { type: String, :null => false }
122
+ column :content, { type: String }
123
+ column :state, { type: Integer }
124
+ column :creation, { type: Date, :locked => true }
125
+ end
126
+
127
+ ### And then? What can I do?
128
+
129
+ #### Load a new model object
130
+
131
+ article = Taupe::Model::Article.load
132
+
133
+ #### Load an existing model object
134
+
135
+ # A database ID
136
+ article_id = 3
137
+
138
+ # An optional cache key. Set it to nil if cache is not setted or needed
139
+ cache_key = "article_#{article_id}"
140
+
141
+ # Load model from database and create corresponding object
142
+ article = Taupe::Model::Article.load article_id, cache_key
143
+
144
+ #### Save a modified model object
145
+
146
+ # Modify the ruby object properties
147
+ article.title = 'This is my modified article title'
148
+
149
+ # Reflect changes in database/cache (insert or update)
150
+ article.save
151
+
152
+ #### Query, query, query. But by yourself.
153
+
154
+ # Execute a single query
155
+ Taupe::Database.exec "INSERT INTO article (title, state) VALUES ('Another article', 1)"
156
+
157
+ # Fetch results in an array
158
+ articles = Taupe::Database.fetch "SELECT * FROM article"
159
+
160
+ # Fetch a single result
161
+ article = Taupe::Database.fetch "SELECT * FROM article WHERE article_id = 3", true
162
+
163
+ For more clarity, you can also exec or fetch from any model class. It is exactly the same.
164
+
165
+ # Execute an insert query
166
+ Taupe::Model::Article.exec "INSERT INTO article (title, state) VALUES ('Another article', 1)"
167
+
168
+ # Fetch
169
+ articles = Taupe::Model::Article.fetch "SELECT * FROM article"
170
+
171
+ # Fetch single
172
+ article = Taupe::Model::Article.fetch "SELECT * FROM article WHERE article_id = 3", true
173
+
174
+ ## License
175
+
176
+ Please see the [LICENSE](https://github.com/pierre-lecocq/taupe/blob/master/LICENSE) file
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ task :default => :help
2
+
3
+ # rake help
4
+ task :help do
5
+ puts 'Taupe - Available tasks:'
6
+ puts ' * test: run rspec tests'
7
+ puts ' * code: run rubocop to verify code quality'
8
+ puts ' * doc: run yard to generate documentation'
9
+ end
10
+
11
+ # rake test
12
+ require 'rspec/core/rake_task'
13
+ RSpec::Core::RakeTask.new(:test)
14
+
15
+ # rake code
16
+ require 'rubocop/rake_task'
17
+ desc 'Run RuboCop on the lib directory'
18
+ RuboCop::RakeTask.new(:code) do |task|
19
+ task.patterns = ['lib/**/*.rb']
20
+ task.fail_on_error = false
21
+ end
22
+
23
+ # rake doc
24
+ require 'yard'
25
+ YARD::Rake::YardocTask.new do |t|
26
+ t.files = ['lib/**/*.rb']
27
+ end
28
+ task :doc => :yard
@@ -0,0 +1,40 @@
1
+ # File: memcached.rb
2
+ # Time-stamp: <2014-09-11 16:30:12 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library memcached driver class
5
+
6
+ module Taupe
7
+ class Cache
8
+ # Memcached cache driver
9
+ class MemcachedDriver
10
+ # Accessors
11
+ attr_accessor :connection
12
+
13
+ # Constructor
14
+ # @param dsn [Hash] The data source name
15
+ def initialize(dsn)
16
+ @connection = Memcached.new dsn
17
+ end
18
+
19
+ # Get a cache entry
20
+ # @param key [String] The key to retrieve
21
+ # @return [Object]
22
+ def get(key)
23
+ @connection.get key.to_s
24
+ end
25
+
26
+ # Set a cache entry
27
+ # @param key [String] The key to set
28
+ # @param value [Object] The value
29
+ def set(key, value)
30
+ @connection.set key.to_s, value
31
+ end
32
+
33
+ # Delete a key
34
+ # @param key [String] The key to delete
35
+ def delete(key)
36
+ @connection.delete key.to_s
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+ # File: redis.rb
2
+ # Time-stamp: <2014-09-11 16:28:45 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library redis driver class
5
+
6
+ module Taupe
7
+ class Cache
8
+ # Redis cache driver
9
+ class RedisDriver
10
+ # Accessors
11
+ attr_accessor :connection
12
+
13
+ # Constructor
14
+ # @param dsn [Hash] The data source name
15
+ def initialize(dsn)
16
+ @connection = Redis.new dsn
17
+ end
18
+
19
+ # Get a cache entry
20
+ # @param key [String] The key to retrieve
21
+ # @return [Object]
22
+ def get(key)
23
+ @connection.hgetall(key).symbolize_keys
24
+ end
25
+
26
+ # Set a cache entry
27
+ # @param key [String] The key to set
28
+ # @param value [Object] The value
29
+ def set(key, value)
30
+ fail 'Only HASH values are supported' unless value.is_a?(Hash)
31
+ @connection.mapped_hmset key, value
32
+ end
33
+
34
+ # Delete a key
35
+ # @param key [String] The key to delete
36
+ def delete(key)
37
+ @connection.del key
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,95 @@
1
+ # -*- coding: utf-8 -*-
2
+ # File: cache.rb
3
+ # Time-stamp: <2014-08-22 16:59:32 pierre>
4
+ # Copyright (C) 2014 Pierre Lecocq
5
+ # Description: Taupe library cache class
6
+
7
+ require 'taupe/cache/memcached'
8
+ require 'taupe/cache/redis'
9
+
10
+ module Taupe
11
+ # Cache class
12
+ # Manage cache connection and serve as query proxy
13
+ class Cache
14
+ # Includes
15
+ include Accessorized
16
+
17
+ # Custom accessors
18
+ # Accessible via _name and _name=
19
+ single_accessor :type, :host, :port
20
+
21
+ # Accessors
22
+ attr_accessor :instance, :driver
23
+
24
+ # Constructor
25
+ # @param block [Proc] A given block
26
+ def initialize(&block)
27
+ instance_eval(&block)
28
+ end
29
+
30
+ # Setup the Cache instance
31
+ # @param block [Proc] A given block
32
+ def self.setup(&block)
33
+ @instance = new(&block)
34
+ setup_defaults
35
+ driver_factory
36
+ end
37
+
38
+ # Setup default values
39
+ def self.setup_defaults
40
+ case @instance._type
41
+ when :memcached
42
+ Taupe.require_gem 'memcached', 'Memcached cache engine'
43
+ @instance._host ||= :localhost
44
+ @instance._port ||= 11_211
45
+ when :redis
46
+ Taupe.require_gem 'redis', 'Redis cache engine'
47
+ @instance._host ||= :localhost
48
+ @instance._port ||= 6379
49
+ else
50
+ fail 'Unknown cache type'
51
+ end
52
+ end
53
+
54
+ # Get the data source name
55
+ # @return [Hash, String]
56
+ def self.dsn
57
+ case @instance._type
58
+ when :memcached
59
+ "#{@instance._host}:#{@instance._port}"
60
+ when :redis
61
+ {
62
+ host: @instance._host.to_s,
63
+ port: @instance._port.to_i
64
+ }
65
+ end
66
+ end
67
+
68
+ # Setup the connection driver
69
+ def self.driver_factory
70
+ cname = "Taupe::Cache::#{@instance._type.capitalize}Driver"
71
+ klass = cname.split('::').reduce(Object) { |a, e| a.const_get e }
72
+ @instance.driver = klass.new dsn
73
+ end
74
+
75
+ # Get a cache entry
76
+ # @param key [String] The key to retrieve
77
+ # @return [Object]
78
+ def self.get(key)
79
+ @instance.driver.get key.to_s
80
+ end
81
+
82
+ # Set a cache entry
83
+ # @param key [String] The key to set
84
+ # @param value [Object] The value
85
+ def self.set(key, value)
86
+ @instance.driver.set key.to_s, value
87
+ end
88
+
89
+ # Delete a key
90
+ # @param key [String] The key to delete
91
+ def delete(key)
92
+ @instance.driver.delete key.to_s
93
+ end
94
+ end
95
+ end
data/lib/taupe/core.rb ADDED
@@ -0,0 +1,74 @@
1
+ # File: core.rb
2
+ # Time-stamp: <2014-08-22 16:34:51 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library core class
5
+
6
+ module Taupe
7
+ # Accessorized module
8
+ # Add the ability to set custom accessor to a class
9
+ module Accessorized
10
+ # Extend ClassMethods
11
+ # @param base [Object] The caller object
12
+ def self.included(base)
13
+ base.extend(ClassMethods)
14
+ end
15
+
16
+ # Class methods
17
+ module ClassMethods
18
+ # Define single accessor. It can be set one time.
19
+ # @example single_accessor :name
20
+ # @note The getter is "_name" instead of "name"
21
+ # @param names [Array] Set of names
22
+ def single_accessor(*names)
23
+ names.each do |name|
24
+ define_method name do |data|
25
+ instance_variable_set "@#{name}".to_sym, data
26
+ end
27
+ define_method "_#{name}" do
28
+ instance_variable_get "@#{name}".to_sym
29
+ end
30
+ define_method "_#{name}=" do |data|
31
+ instance_variable_set "@#{name}".to_sym, data
32
+ end
33
+ end
34
+ end
35
+
36
+ # Define stacked accessor. It can be set several times.
37
+ # @example stacked_accessor :name, { type; String, null; false }
38
+ # @note The getter is "_name_stack" instead of "name"
39
+ # @param names [Array] Set of names, and a hash of values
40
+ def stacked_accessor(*names)
41
+ names.each do |name|
42
+ define_method name do |*data|
43
+ stack = instance_variable_get("@_#{name}_stack".to_sym) || {}
44
+ stack[data[0]] = data[1]
45
+ instance_variable_set "@_#{name}_stack".to_sym, stack
46
+ end
47
+ define_method "_#{name}_stack" do
48
+ instance_variable_get "@_#{name}_stack".to_sym
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # Hash ruby core class monkey patching (yes, this is ugly)
57
+ class Hash
58
+ # Symbolize keys (credits to Avdi Grimm)
59
+ def symbolize_keys
60
+ each_with_object({}) do |(key, value), result|
61
+ new_key = case key
62
+ when String then key.to_sym
63
+ else key
64
+ end
65
+ new_val = case value
66
+ when Hash then symbolize_keys(value)
67
+ else value
68
+ end
69
+ result[new_key] = new_val
70
+
71
+ result
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,70 @@
1
+ # File: mysql.rb
2
+ # Time-stamp: <2014-09-11 16:29:52 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library mysql driver class
5
+
6
+ module Taupe
7
+ class Database
8
+ # Mysql database driver
9
+ class MysqlDriver
10
+ # Accessors
11
+ attr_accessor :connection
12
+
13
+ # Constructor
14
+ # @param dsn [Hash] The data source name
15
+ def initialize(dsn)
16
+ dsn[:host] = '127.0.0.1' if dsn[:host].to_s == 'localhost'
17
+ @connection = Mysql2::Client.new dsn
18
+ @connection.query_options.merge! symbolize_keys: true
19
+ end
20
+
21
+ # Execute a single query
22
+ # @param query [String] The query to execute
23
+ # @return [Object]
24
+ def exec(query)
25
+ @connection.query query
26
+ end
27
+
28
+ # Fetch objects from database
29
+ # @param query [String] The query to fetch
30
+ # @return [Array, Object]
31
+ def fetch(query)
32
+ exec(query).to_a
33
+ end
34
+
35
+ # Get last inserted id
36
+ # @return [Integer]
37
+ def last_id
38
+ @connection.last_id.to_i
39
+ end
40
+
41
+ # Guess schema of a table
42
+ # @param table [String] The table name
43
+ # @return [Hash]
44
+ def guess_schema(table)
45
+ results = {}
46
+
47
+ query = format('SHOW COLUMNS FROM %s', table)
48
+
49
+ fetch(query).each do |values|
50
+ type = Taupe::Validate.standardize_sql_type values[:Type]
51
+
52
+ results[values[:Field].to_sym] = {
53
+ type: type,
54
+ null: values[:Null] != 'NO',
55
+ primary_key: values[:Key] == 'PRI'
56
+ }
57
+ end
58
+
59
+ results
60
+ end
61
+
62
+ # Escape a string
63
+ # @param str [String]
64
+ # @return [String]
65
+ def escape(str)
66
+ @connection.escape str
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,89 @@
1
+ # File: postgresql.rb
2
+ # Time-stamp: <2014-09-11 16:29:13 pierre>
3
+ # Copyright (C) 2014 Pierre Lecocq
4
+ # Description: Taupe library postgresql driver class
5
+
6
+ module Taupe
7
+ class Database
8
+ # Postgresql database driver
9
+ class PostgresqlDriver
10
+ # Accessors
11
+ attr_accessor :connection, :last_id
12
+
13
+ # Constructor
14
+ # @param dsn [Hash] The data source name
15
+ def initialize(dsn)
16
+ @connection = PG::Connection.new dsn
17
+ end
18
+
19
+ # Execute a single query
20
+ # @param query [String] The query to execute
21
+ # @return [Object]
22
+ def exec(query)
23
+ result = @connection.exec query
24
+
25
+ @last_id = nil
26
+ @last_id = result[0].flatten[0] if query.upcase.include?('RETURNING')
27
+
28
+ result
29
+ end
30
+
31
+ # Fetch objects from database
32
+ # @param query [String] The query to fetch
33
+ # @return [Array, Object]
34
+ def fetch(query)
35
+ exec(query).to_a.map(&:symbolize_keys)
36
+ end
37
+
38
+ # Get last inserted id
39
+ # @return [Integer]
40
+ def last_id
41
+ if @last_id.nil?
42
+ message = 'Last ID can not be retrieved.'
43
+ message << ' Maybe the last query did not include "RETURNING"'
44
+
45
+ warn message
46
+ end
47
+
48
+ @last_id.to_i
49
+ end
50
+
51
+ # Guess schema of a table
52
+ # @param table [String] The table name
53
+ # @return [Hash]
54
+ def guess_schema(table)
55
+ results = {}
56
+
57
+ query = 'SELECT'
58
+ query << ' column_name, data_type, character_maximum_length,'
59
+ query << ' column_default, is_nullable'
60
+ query << ' FROM INFORMATION_SCHEMA.COLUMNS'
61
+ query << format(' WHERE table_name = \'%s\'', table)
62
+ query << ' ORDER BY ordinal_position'
63
+
64
+ fetch(query).each do |values|
65
+ type = Taupe::Validate.standardize_sql_type values[:data_type]
66
+ pkey = false
67
+ unless values[:column_default].nil?
68
+ pkey = true unless values[:column_default].match('nextval').nil?
69
+ end
70
+
71
+ results[values[:column_name].to_sym] = {
72
+ type: type,
73
+ null: values[:is_nullable] != 'NO',
74
+ primary_key: pkey
75
+ }
76
+ end
77
+
78
+ results
79
+ end
80
+
81
+ # Escape a string
82
+ # @param str [String]
83
+ # @return [String]
84
+ def escape(str)
85
+ @connection.escape_string str
86
+ end
87
+ end
88
+ end
89
+ end