Skip to content

Commit

Permalink
Merge pull request #1 from Sage/initial_code
Browse files Browse the repository at this point in the history
Initial code
  • Loading branch information
chrisbarber86 authored Jul 3, 2018
2 parents e454f41 + 57fb7e0 commit 7d89b7c
Show file tree
Hide file tree
Showing 31 changed files with 1,789 additions and 1 deletion.
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
language: ruby
services:
- mysql
- redis-server
rvm:
- 2.3.5
- 2.5.0
before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64
> ./cc-test-reporter
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ group :test, :development do
end

group :test do
gem 'guard'
gem 'guard-rspec'
gem 'simplecov', require: false
end
9 changes: 9 additions & 0 deletions Guardfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

# More info at https://github.com/guard/guard#readme
guard :rspec, cmd: 'rspec' do
watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch(%r{^lib/mysql_framework/(.+)\.rb$}) { |m| "spec/lib/mysql_framework/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
end
259 changes: 259 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,265 @@

Welcome to Mysql_Framework, this is a light weight framework that provides managers to help with interacting with mysql

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'mysql_framework'
```

## Usage

### Environment Variables

* `MYSQL_HOST` - MySQL Host
* `MYSQL_PORT` - MySQL Port
* `MYSQL_DATABASE` - MySQL database name
* `MYSQL_USERNAME` - MySQL username
* `MYSQL_PASSWORD` - MySQL password
* `MYSQL_PARTITIONS` - The number of partitions where a table is partitioned (default: `500`)
* `REDIS_URL` - The URL for redis - used for managing locks for DB migrations

### Migration Scripts

Migration scripts need to be in the following format:

```ruby
class CreateDemoTable < MysqlFramework::Scripts::Base
def initialize
@identifier = 201806021520 # 15:20 02/06/2018
end

def apply
mysql_connector.query(<<~SQL)
CREATE TABLE IF NOT EXISTS `#{database_name}`.`demo` (
`id` CHAR(36) NOT NULL,
`name` VARCHAR(255) NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`)
)
SQL
end

def rollback
mysql_connector.query(<<~SQL)
DROP TABLE IF EXISTS `#{database_name}`.`demo`
SQL
end

def tags
[DemoTable::NAME]
end
end
```

#### #initialize

The initialize method should set the `@identifier` value, which should be a timestamp

```ruby
@identifier = 201806021520 # 15:20 02/06/2018
```

#### #apply

The `apply` method should action the migration. An instance of the `MysqlFramework::Connector` is
available as `mysql_connector` to use.

#### #rollback

The `rollback` method should action the migration. An instance of the `MysqlFramework::Connector`
is available as `mysql_connector` to use.

#### #tags

Tags are used for when we want to specify which migrations to run based on a tag. This is useful
for tests where you don't need to run all migrations to assert something is working or not.

### MysqlFramework::Scripts::Table

Used to register tables. This is used as part of the `all_tables` method in the script manager for
awareness of tables to drop.

```ruby
class DemoTable
extend MysqlFramework::Scripts::Table

NAME = 'demo'

register_table NAME
end
```

### MysqlFramework::Connector

The connector deals with the connection pooling of `MySQL2::Client` instances, providing a wrapper for queries and transactions.

```ruby
connector = MysqlFramework::Connector.new
connector.query(<<~SQL)
SELECT * FROM gems
SQL
```

Options can be provided to override the defaults as follows:

```ruby
options = {
host: ENV.fetch('MYSQL_HOST'),
port: ENV.fetch('MYSQL_PORT'),
database: ENV.fetch('MYSQL_DATABASE'),
username: ENV.fetch('MYSQL_USERNAME'),
password: ENV.fetch('MYSQL_PASSWORD'),
reconnect: true
}
MysqlFramework::Connector.new(options)
```

#### #check_out

Check out a client from the connection pool

```ruby
client = connector.check_out
```

#### #check_in

Check in a client to the connection pool

```ruby
client = connector.check_out
# ...
connector.check_in(client)
```

#### #with_client

Called with a block. The method checks out a client from the pool and yields it to the block. Finally it ensures that the client is always checked back into the pool.

```ruby
connector.with_client do |client|
client.query(<<~SQL)
SELECT * FROM gems
SQL
end
```

#### #execute

This method is called when executing a prepared statement where value substitution is required:

```ruby
insert = MysqlFramework::SqlQuery.new.insert(gems)
.into(gems[:id],gems[:name],gems[:author],gems[:created_at],gems[:updated_at])
.values(SecureRandom.uuid,'mysql_framework','sage',Time.now,Time.now)

connector.execute(insert)
```

#### #query

This method is called to execute a query without having to worry about obtaining a client

```ruby
connector.query(<<~SQL)
SELECT * FROM versions
SQL
```

#### #transaction

This method requires a block and yields a client obtained from the pool. It wraps the yield in a `BEGIN` and `COMMIT` query. If an exception is raised then it will submit a `ROLLBACK` query and re-raise the exception.

```ruby
insert = MysqlFramework::SqlQuery.new.insert(gems)
.into(gems[:id],gems[:name],gems[:author],gems[:created_at],gems[:updated_at])
.values(SecureRandom.uuid,'mysql_framework','sage',Time.now,Time.now)

connector.transaction do |client|
client.query(insert)
end
```

#### #default_options

The default options used to initialise MySQL2::Client instances:

```ruby
{
host: ENV.fetch('MYSQL_HOST'),
port: ENV.fetch('MYSQL_PORT'),
database: ENV.fetch('MYSQL_DATABASE'),
username: ENV.fetch('MYSQL_USERNAME'),
password: ENV.fetch('MYSQL_PASSWORD'),
reconnect: true
}
```

### MysqlFramework::SqlCondition

A representation of a MySQL Condition for a column. Created automatically by SqlColumn

```ruby
# eq condition
SqlCondition.new(column: 'name', comparison: '=', value: 'mysql_framework')
```

### MysqlFramework::SqlColumn

A representation of a MySQL column within a table. Created automatically by SqlTable.

```ruby
SqlCondition.new(table: 'gems', column: 'name')
```

### MysqlFramework::SqlQuery

A representation of a MySQL Query.

```ruby
gems = MysqlFramework::SqlTable.new('gems')
guid = SecureRandom.uuid

# Insert Query
insert = MysqlFramework::SqlQuery.new.insert(gems)
.into(gems[:id],gems[:name],gems[:author],gems[:created_at],gems[:updated_at])
.values(guid,'mysql_framework','sage',Time.now,Time.now)

# Update Query
update = MysqlFramework::SqlQuery.new.update(gems)
.set(updated_at: Time.now)
.where(gems[:id].eq(guid))

# Delete Query
delete = MysqlFramework::SqlQuery.new.delete
.from(gems)
.where(gems[:id].eq(guid))
```

### MysqlFramework::SqlTable

A representation of a MySQL table.

```ruby
MysqlFramework::SqlTable.new('gems')
```

### Configuring Logs

As a default, `MysqlFramework` will log to `STDOUT`. You can provide your own logger using the `set_logger` method:

```ruby
MysqlFramework.set_logger(Logger.new('development.log'))
```

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/sage/mysql_framework. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.

## License

This gem is available as open source under the terms of the [MIT licence](LICENSE).
Expand Down
10 changes: 10 additions & 0 deletions lib/mysql_framework.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
require 'mysql2'
require 'redlock'

require_relative 'mysql_framework/connector'
require_relative 'mysql_framework/logger'
require_relative 'mysql_framework/scripts'
require_relative 'mysql_framework/sql_column'
require_relative 'mysql_framework/sql_condition'
require_relative 'mysql_framework/sql_query'
require_relative 'mysql_framework/sql_table'
require_relative 'mysql_framework/version'

module MysqlFramework
Expand Down
84 changes: 84 additions & 0 deletions lib/mysql_framework/connector.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

module MysqlFramework
class Connector
def initialize(options = {})
@connection_pool = ::Queue.new

@options = default_options.merge(options)

Mysql2::Client.default_query_options.merge!(symbolize_keys: true, cast_booleans: true)
end

# This method is called to fetch a client from the connection pool or create a new client if no idle clients
# are available.
def check_out
@connection_pool.pop(true)
rescue StandardError
Mysql2::Client.new(@options)
end

# This method is called to check a client back in to the connection when no longer needed.
def check_in(client)
@connection_pool.push(client)
end

# This method is called to use a client from the connection pool.
def with_client
client = check_out
yield client
ensure
check_in(client) unless client.nil?
end

# This method is called to execute a prepared statement
def execute(query)
with_client do |client|
statement = client.prepare(query.sql)
statement.execute(*query.params)
end
end

# This method is called to execute a query
def query(query_string)
with_client { |client| client.query(query_string) }
end

# This method is called to execute a query which will return multiple result sets in an array
def query_multiple_results(query_string)
with_client do |client|
result = []
result << client.query(query_string).to_a
result << client.store_result&.to_a while client.next_result
result.compact
end
end

# This method is called to use a client within a transaction
def transaction
raise ArgumentError, 'No block was given' unless block_given?

with_client do |client|
begin
client.query('BEGIN')
yield client
client.query('COMMIT')
rescue StandardError => e
client.query('ROLLBACK')
raise e
end
end
end

def default_options
{
host: ENV.fetch('MYSQL_HOST'),
port: ENV.fetch('MYSQL_PORT'),
database: ENV.fetch('MYSQL_DATABASE'),
username: ENV.fetch('MYSQL_USERNAME'),
password: ENV.fetch('MYSQL_PASSWORD'),
reconnect: true
}
end
end
end
Loading

0 comments on commit 7d89b7c

Please sign in to comment.