Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
require 'promise' | ||
require 'ostruct' | ||
|
||
module Browser; module Database | ||
|
||
class SQL | ||
# Check if the browser supports WebSQL. | ||
def self.supported? | ||
Browser.supports? 'WebSQL' | ||
end | ||
|
||
class Error < StandardError | ||
def self.new(error) | ||
return super if self != Error | ||
|
||
[Unknown, Database, Version, TooLarge, Quota, Syntax, Constraint, Timeout] \ | ||
[`error.code`].new(`error.message`) | ||
end | ||
|
||
Unknown = Class.new(self) | ||
Database = Class.new(self) | ||
Version = Class.new(self) | ||
TooLarge = Class.new(self) | ||
Quota = Class.new(self) | ||
Syntax = Class.new(self) | ||
Constraint = Class.new(self) | ||
Timeout = Class.new(self) | ||
end | ||
|
||
include Native | ||
|
||
# @return [String] the name of the database | ||
attr_reader :name | ||
|
||
# @return [String] the description for the database | ||
attr_reader :description | ||
|
||
# @return [Integer] the size constraint in bytes | ||
attr_reader :size | ||
|
||
# Open a database with the given name and options. | ||
# | ||
# @param name [String] the name for the database | ||
# @param options [Hash] options to open the database | ||
# | ||
# @option options [String] :description the description for the database | ||
# @option options [String] :version ('') the expected version of the database | ||
# @option options [Integer] :size (5 * 1024 * 1024) the size constraint in bytes | ||
def initialize(name, options = {}) | ||
@name = name | ||
@description = options[:description] || name | ||
@version = options[:version] || '' | ||
@size = options[:size] || 5 * 1024 * 1024 | ||
|
||
super(`window.openDatabase(#{name}, #{@version}, #{@description}, #{@size})`) | ||
end | ||
|
||
# @overload version() | ||
# | ||
# Get the version of the database. | ||
# | ||
# @return [String] | ||
# | ||
# @overload version(from, to, &block) | ||
# | ||
# Migrate the database to a new version. | ||
# | ||
# @param from [String] the version you're migrating from | ||
# @param to [String] the version you're migrating to | ||
# | ||
# @yieldparam transaction [Transaction] the transaction to work with | ||
def version(from = nil, to = nil, &block) | ||
return `#@native.version` unless block | ||
|
||
`#@native.changeVersion(#{from}, #{to}, | ||
#{-> t { block.call(Transaction.new(self, t)) }})` | ||
end | ||
|
||
# Start a transaction on the database. | ||
# | ||
# @yieldparam transaction [Transaction] the transaction to work on | ||
def transaction(&block) | ||
raise ArgumentError, 'no block given' unless block | ||
|
||
`#@native.transaction(#{-> t { block.call(Transaction.new(self, t)) }})` | ||
end | ||
|
||
# Allows you to make changes to the database or read data from it. | ||
class Transaction | ||
include Native | ||
|
||
# @return [Database] the database the transaction has been created from | ||
attr_reader :database | ||
|
||
# @private | ||
def initialize(database, transaction) | ||
@database = database | ||
|
||
super(transaction) | ||
end | ||
|
||
# Query the database. | ||
# | ||
# @param query [String] the SQL query to send | ||
# @param parameters [Array] optional bind parameters for the query | ||
# | ||
# @return [Promise] | ||
def query(query, *parameters) | ||
promise = Promise.new | ||
|
||
`#@native.executeSql(#{query}, #{parameters}, | ||
#{-> _, r { promise.resolve(Result.new(self, r)) }}, | ||
#{-> _, e { promise.reject(Error.new(e)) }})` | ||
|
||
promise | ||
end | ||
end | ||
|
||
# Represents a row. | ||
class Row < OpenStruct | ||
# @private | ||
def initialize(row) | ||
super(Hash.new(row)) | ||
end | ||
|
||
def inspect | ||
"#<SQL::Row: #{Hash.new(@native).inspect}>" | ||
end | ||
end | ||
|
||
class Result | ||
include Native | ||
|
||
# @return [Transaction] the transaction the result came from | ||
attr_reader :transaction | ||
|
||
# @return [SQL] the database the result came from | ||
attr_reader :database | ||
|
||
# @private | ||
def initialize(transaction, result) | ||
@transaction = transaction | ||
@database = transaction.database | ||
|
||
super(result) | ||
end | ||
|
||
include Enumerable | ||
|
||
# Get a row from the result. | ||
# | ||
# @param index [Integer] the index for the row | ||
# | ||
# @return [Row] | ||
def [](index) | ||
if index < 0 | ||
index += length | ||
end | ||
|
||
unless index < 0 || index >= length | ||
Row.new(`#@native.rows.item(index)`) | ||
end | ||
end | ||
|
||
# Enumerate over the rows. | ||
# | ||
# @yieldparam row [Row] | ||
# | ||
# @return [self] | ||
def each(&block) | ||
return enum_for :each unless block | ||
|
||
%x{ | ||
for (var i = 0, length = #@native.rows.length; i < length; i++) { | ||
#{block.call(self[`i`])}; | ||
} | ||
} | ||
|
||
self | ||
end | ||
|
||
# @!attribute [r] length | ||
# @return [Integer] number of rows in the result | ||
def length | ||
`#@native.rows.length` | ||
end | ||
|
||
# @!attribute [r] affected | ||
# @return [Integer] number of affected rows | ||
alias_native :affected, :rowsAffected | ||
end | ||
end | ||
|
||
end; end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
require 'spec_helper' | ||
require 'browser/database/sql' | ||
|
||
describe Browser::Database::SQL do | ||
SQL = Browser::Database::SQL | ||
|
||
describe '#new' do | ||
it 'sets the attributes properly' do | ||
sql = SQL.new('test', description: 'trains', size: 1024) | ||
|
||
expect(sql.name).to eq('test') | ||
expect(sql.description).to eq('trains') | ||
expect(sql.size).to eq(1024) | ||
end | ||
|
||
it 'sets the version properly' do | ||
sql = SQL.new('test2', version: '1.0') | ||
expect(sql.version).to eq('1.0') | ||
|
||
sql = SQL.new('test') | ||
expect(sql.version).to eq('') | ||
end | ||
end | ||
|
||
describe '#transaction' do | ||
async 'calls the block with the transaction' do | ||
sql = SQL.new('test') | ||
|
||
sql.transaction {|t| | ||
async { | ||
expect(t).to be_a(SQL::Transaction) | ||
} | ||
} | ||
end | ||
|
||
async 'the transaction database is the right one' do | ||
sql = SQL.new('test') | ||
|
||
sql.transaction {|t| | ||
async { | ||
expect(t.database).to eq(sql) | ||
} | ||
} | ||
end | ||
end | ||
|
||
describe SQL::Transaction do | ||
describe '#query' do | ||
async 'returns a promise' do | ||
sql = SQL.new('test') | ||
|
||
sql.transaction {|t| | ||
async { | ||
expect(t.query('hue')).to be_a(Promise) | ||
} | ||
} | ||
end | ||
|
||
async 'resolves on success' do | ||
sql = SQL.new('test') | ||
|
||
sql.transaction {|t| | ||
t.query('CREATE TABLE IF NOT EXISTS test(ID INTEGER PRIMARY KEY ASC, a TEXT)').then {|r| | ||
async { | ||
expect(r).to be_a(SQL::Result) | ||
} | ||
} | ||
} | ||
end | ||
|
||
async 'rejects on failure' do | ||
sql = SQL.new('test') | ||
|
||
sql.transaction {|t| | ||
t.query('huehue').rescue {|e| | ||
async { | ||
expect(e).to be_a(SQL::Error::Syntax) | ||
} | ||
} | ||
} | ||
end | ||
end | ||
end | ||
|
||
describe SQL::Result do | ||
describe '#length' do | ||
async 'has the proper length' do | ||
sql = SQL.new('test') | ||
|
||
sql.transaction {|t| | ||
t.query('SELECT 1').then {|r| | ||
async { | ||
expect(r.length).to eq(1) | ||
} | ||
} | ||
} | ||
end | ||
end | ||
|
||
describe '#[]' do | ||
async 'returns a row' do | ||
sql = SQL.new('test') | ||
|
||
sql.transaction {|t| | ||
t.query('SELECT 1, 2, 3').then {|r| | ||
async { | ||
expect(r[0]).to be_a(SQL::Row) | ||
expect(r[-1]).to be_a(SQL::Row) | ||
|
||
expect(r[0]).to eq(r[-1]) | ||
} | ||
} | ||
} | ||
end | ||
|
||
async 'returns nil on missing row' do | ||
sql = SQL.new('test') | ||
|
||
sql.transaction {|t| | ||
t.query('SELECT 1, 2, 3').then {|r| | ||
async { | ||
expect(r[5]).to be_nil | ||
expect(r[-5]).to be_nil | ||
} | ||
} | ||
} | ||
end | ||
end | ||
end | ||
end if Browser::Database::SQL.supported? |