Sequel::Model: Where the methods like create_table come from? - ruby

I am trying to understand how Sequel works. The example below inherit from Sequel::Model and calls set_schema, create_table, etc.
I was trying to find the documentation for these methods, but no luck on the rdoc for Sequel::Model: http://sequel.rubyforge.org/rdoc/classes/Sequel/Model.html
Where are these methods coming from and how does Sequel::Model make them available?
class Task < Sequel::Model
set_schema do
primary_key :id
varchar :title, :unique => true, :empty => false
boolean :done, :default => false
end
create_table unless table_exists?
if empty?
create :title => 'Laundry'
create :title => 'Wash dishes'
end
end

They're defined in Sequel::Plugins::Schema::ClassMethods (lib/sequel/plugins/schema.rb) and included when you call plugin :schema in your model.
http://sequel.rubyforge.org/rdoc-plugins/classes/Sequel/Plugins/Schema/ClassMethods.html#M000110
http://sequel.rubyforge.org/rdoc/classes/Sequel/Model.html#M000130
The example in the question is incomplete and won't work unless a connection to a database is setup and the plugin :schema is called from the model.
irb(main):001:0> require "rubygems"
=> true
irb(main):002:0> require "sequel"
=> true
irb(main):003:0>
irb(main):004:0* # connect to an in-memory database
irb(main):005:0* DB = Sequel.sqlite
=> #<Sequel::SQLite::Database: "sqlite:/">
irb(main):006:0> class Task < Sequel::Model
irb(main):007:1> set_schema do
irb(main):008:2* primary_key :id
irb(main):009:2>
irb(main):010:2* varchar :title, :unique => true, :empty => false
irb(main):011:2> boolean :done, :default => false
irb(main):012:2> end
irb(main):013:1>
irb(main):014:1* create_table unless table_exists?
irb(main):015:1>
irb(main):016:1* if empty?
irb(main):017:2> create :title => 'Laundry'
irb(main):018:2> create :title => 'Wash dishes'
irb(main):019:2> end
irb(main):020:1> end
NoMethodError: undefined method `set_schema' for Task:Class
from (irb):7
irb(main):021:0> class Task < Sequel::Model
irb(main):022:1> plugin :schema
irb(main):023:1> set_schema do
irb(main):024:2* primary_key :id
irb(main):025:2>
irb(main):026:2* varchar :title, :unique => true, :empty => false
irb(main):027:2> boolean :done, :default => false
irb(main):028:2> end
irb(main):029:1>
irb(main):030:1* create_table unless table_exists?
irb(main):031:1>
irb(main):032:1* if empty?
irb(main):033:2> create :title => 'Laundry'
irb(main):034:2> create :title => 'Wash dishes'
irb(main):035:2> end
irb(main):036:1> end
=> #<Task #values={:title=>"Wash dishes", :done=>false, :id=>2}>
irb(main):037:0>

Related

Reproduce Sequel's validates_min_length validation

I'm hoping to reproduce Sequel's validates_min_length errors for a Bcrypt-encrypted password. I can't use the validation as it will test the password hash, rather than the un-encrypted text.
I'm having difficulty getting the password= method to produce the desired errors.
Un-encrypted-password logic
require 'sequel'
DB = Sequel.sqlite
DB.create_table(:users) do
primary_key :id
String :name, null: false, unique: true
String :password, null: false
end
class User < Sequel::Model
plugin :validation_helpers
def validate
super
validates_presence [:name,:password]
validates_unique [:name]
validates_min_length 8, :password
end
end
IRB:
irb(main):001:0> u=User.new(name: 'foobar', password: 'Pa55w0rd')
=> #<User #values={:name=>"foobar", :password=>"Pa55w0rd"}>
irb(main):002:0> u.valid?
=> true
irb(main):003:0> u.password=nil
=> nil
irb(main):004:0> u
=> #<User #values={:name=>"foobar", :password=>nil}>
irb(main):005:0> u.valid?
=> false
irb(main):007:0> u.errors
=> {:password=>["is not present", "is shorter than 8 characters"]}
irb(main):008:0> u.password='foo'
=> "foo"
irb(main):009:0> u
=> #<User #values={:name=>"foobar", :password=>"foo"}>
irb(main):010:0> u.valid?
=> false
irb(main):011:0> u.errors
=> {:password=>["is shorter than 8 characters"]}
Encrypted-password logic
require 'sequel'
require 'bcrypt'
DB = Sequel.sqlite
DB.create_table(:users) do
primary_key :id
String :name, null: false, unique: true
String :password_hash, null: false
end
class User < Sequel::Model
plugin :validation_helpers
include BCrypt
def validate
super
validates_presence [:name,:password]
validates_unique [:name]
end
def password
# check for :password_hash existence to ensure that validates_presence :password works correctly
#password ||= Password.new(password_hash) if password_hash
end
def password=(new_password)
# add validation errors
errors.add(:password, 'is shorter than 8 characters') if new_password==nil || new_password.length < 8
if new_password == nil
#password = nil
else
#password = Password.create(new_password)
end
self.password_hash = #password
end
end
IRB:
irb(main):001:0> u=User.new(name: 'foobar', password: 'Pa55w0rd')
=> #<User #values={:name=>"foobar", :password_hash=>"$2a$10$K3UALPYz/bb5bdrGmbq22eRM31A6rU3kqkbzcU4.6J5APQVSqxQo6"}>
irb(main):002:0> u.valid?
=> true
irb(main):003:0> u.password=nil
=> nil
irb(main):004:0> u
=> #<User #values={:name=>"foobar", :password_hash=>nil}>
irb(main):005:0> u.valid?
=> false
irb(main):006:0> u.errors
=> {:password=>["is not present"]}
irb(main):007:0> u.password='foo'
=> "foo"
irb(main):009:0> u
=> #<User #values={:name=>"foobar", :password_hash=>"$2a$10$lA6fsKXSvl5cd.Zl53qEqOzxk1LPehvGujWaXwcf1//IUc82CmowC"}>
irb(main):008:0> u.valid?
=> true
Both invalid passwords (nil,'foo') are missing the is shorter than 8 characters error.
What am I missing?
Versions:
$ sequel --version
sequel 5.7.1
#password is not actually shorter than 8 characters. BCrypt::Password is a subclass of string, and it will have the same length as the password hash. You would have to set #password in password= if you wanted to use a validation to ensure the size of the password.
I am not 100% sure on functionality but this should work for you.
def validate
super
validates_presence [:name,:password]
validates_unique [:name]
validates_min_length 8, :password
end
def password=(new_password)
return unless #password = new_password and #password.to_s.length >= 8
self.password_hash = Password.create(password)
end
I added the actual length validation to validate so that valid? will return false. Additionally I cleaned up the password= method a bit.
So now password= has a guard clause that simply returns if the password is invalid as defined by "less than 8 characters".
Otherwise we create a new password_hash via Password.create and assign it to #password_hash.
Please note: There is a functional change between my proposed code and yours which is that #password_hash is not overwritten when the "new_password" is invalid. This seemed counter intuitive to me that an invalid password could overwrite an existing and valid password_hash thus my change in implementation.
All that being said your best bet is actually to go with this
class User < Sequel::Model
plugin :validation_helpers
include BCrypt
attr_accessor :password
def validate
super
validates_presence [:name,:password]
validates_unique [:name]
validates_min_length 8, :password
end
def before_save
self.password_hash = Password.create(password)
end
end
This avoids all the manipulation and will only create a password_hash (which is all you actually need) if the model passes validation checks
This works as desired:
require 'sequel'
require 'bcrypt'
DB = Sequel.sqlite
DB.create_table(:users) do
primary_key :id
String :name, null: false, unique: true
String :password_hash, null: false
end
class User < Sequel::Model
plugin :validation_helpers
include BCrypt
def validate
super
validates_presence [:name,:password]
validates_unique [:name]
errors.add(:password, 'is shorter than 8 characters') if #password==nil || #password.length < 8
end
def password
Password.new(password_hash) if password_hash
end
def password=(new_password)
# uncomment to prevent bad passwords from changing a good password; probably needs to include a non-terminating error message
# return if new_password==nil || new_password.length < 8
# store clear-text password for validation
#password = new_password
#modify password hash accordingly
self.password_hash = if new_password then Password.create(new_password) else nil end
end
end

Sequel makes duplicate entries

I'm using Sequel with Sinatra and I've been trying it out and I noticed that it creates duplicate entries (3x).I've tried to debug it but in the end I couldn't understand why is this occurring.
This is my code:
require 'sinatra'
require 'mysql2'
require 'sequel'
DB=Sequel.connect(
:adapter => "mysql2",
:host =>"localhost",
:user => "root",
:password => "",
:database => "test"
)
if !DB.table_exists? :users
DB.create_table :users do
primary_key :id
String :username ,:size=>20 #,:unique=>true
String :password ,:size=>30
end
puts "Schema created"
else
puts "Already exists"
end
class User < Sequel::Model;end
class Test < Sinatra::Base
get '/:username' do
"Creating user"
User.create(:username => "#{params['username']}",:password => "test")
end
end

Advanced count association broken after sequel upgrade

I created an association in order to reduce the number of queries in a construct like this
#user.all do |user|
puts "User ##{user.id}, post count: #{user.posts_count}"
end
My Model:
class User
one_to_many :posts
one_to_one :posts_count, :read_only => true, :key => :id,
:dataset => proc{Post.where(:user_id => :id, :deleted => false).select{count(Sequel.lit("*")).as(count)}},
:eager_loader => (proc do |eo|
eo[:rows].each{|p| p.associations[:posts_count] = nil}
Post.where(:user_id => eo[:id_map].keys, :deleted => false).
select_group(:user_id).
select_append{count(Sequel.lit("*")).as(count)}.
all do |t|
p = eo[:id_map][t.values.delete(:user_id)].first
p.associations[:posts_count] = t
end
end)
def posts_count
super.try(:[], :count) || 0
end
# ...
end
After upgrading to sequel 4.1.0 I get unitialized constant PostsCount, while in sequel 3.44 it worked fine. How can I fix this?
Adding a class attribute solves the issue. Include :class => "Post" like this:
one_to_one :posts_count, :read_only => true, :key => :id, :class => "Post",
#...

Top level constant referenced by warning for Mongoid model

I have the following mongoid model that inherits from the Entry model:
class Entry::Twitter < Entry
field :retweet_count, :type => Integer, :default => 0
field :retweeted, :type => Boolean, :default => false
field :favorited, :type => Boolean, :default => false
# in_reply_to_screen_name, in_reply_to_status_id_str, in_reply_to_user_id_str
field :reply, :type => Hash
field :from, :type => Hash # user: id_str, name, screen_name
field :time, :type => Time # created_at
field :data, :type => Hash # entities (hashtags and user_mentions)
field :assets, :type => Hash # urls from original entities
field :service, :type => String, :default => "twitter"
attr_accessible :assets
# validations
validates_presence_of :retweet_count, :from, :time, :data
# override set_service cause of https://github.com/sferik/twitter/issues/303
def set_service
self.service = "twitter"
end
end
When i try to reference it i get the following warning:
ruby-1.9.3-p125 :001 > Entry::Twitter
(irb):1: warning: toplevel constant Twitter referenced by Entry::Twitter
=> Twitter
Instead of referencing to my model it references to the Top Level Constant Twitter that is defined by a gem.
What can i do to fix this? I don't want to use another name for my class.
here is the solution:
https://github.com/rails/rails/issues/6931
I just added require_dependency 'entry/twitter' to every files that references Entry::Twitter to avoid this problem, and it works fine now.

User model test error from inserting into Group table

I'm just starting out with tests. When I run this one:
rake test test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
test "should not save without an email address" do
user = User.new
assert_not user.save
end
end
I get the following error:
1) Error:
UserTest#test_should_not_save_without_an_email_address:
ActiveRecord::StatementInvalid: SQLite3::ConstraintException: NOT NULL constraint failed: groups.name: INSERT INTO "groups" ("created_at", "updated_at", "id") VALUES ('2015-08-11 17:31:07', '2015-08-11 17:31:07', 980190962)
This is user.rb
class User < ActiveRecord::Base
has_many :groups
has_many :user_groups
attr_accessor :password
EMAIL_REGEX = /A[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}\z/i
validates :password, :confirmation => true #password_confirmation attr
validates_length_of :password, :in => 6..20, :on => :create
validates :email, :presence => true, :uniqueness => true, :format => EMAIL_REGEX
before_save :encrypt_password
after_save :clear_password
def encrypt_password
if password.present?
self.salt = Digest::SHA1.hexdigest("# We add {self.email} as unique value and #{Time.now} as random value")
self.encrypted_password = Digest::SHA1.hexdigest("Adding #{self.salt} to {password}")
end
end
def clear_password
self.password = nil
end
end
This is test_helper.rb
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
# Add more helper methods to be used by all tests here...
end
As far as I can tell I don't have any callback or otherwise that would attempt to write to the "groups" table. My "groups.yml" is default, but that shouldn't matter if I'm only testing this one model, correct? Any help as to where I could start looking would be much appreciated. Thanks!
test_helper.rb was setting up all my fixtures and they weren't defined. Commenting "fixtures :all" fixed it.

Resources