ActiveRecord/sqlite3 column type lost in table view? - ruby

I have the following ActiveRecord testcase that mimics my problem. I have a People table with one attribute being a date. I create a view over that table adding one column which is just that date plus 20 minutes:
#!/usr/bin/env ruby
%w|pp rubygems active_record irb active_support date|.each {|lib| require lib}
ActiveRecord::Base.establish_connection(
:adapter => "sqlite3",
:database => "test.db"
)
ActiveRecord::Schema.define do
create_table :people, :force => true do |t|
t.column :name, :string
t.column :born_at, :datetime
end
execute "create view clowns as select p.name, p.born_at, datetime(p.born_at, '+' || '20' || ' minutes') as twenty_after_born_at from people p;"
end
class Person < ActiveRecord::Base
validates_presence_of :name
end
class Clown < ActiveRecord::Base
end
Person.create(:name => "John", :born_at => DateTime.now)
pp Person.all.first.born_at.class
pp Clown.all.first.born_at.class
pp Clown.all.first.twenty_after_born_at.class
The problem is, the output is
Time
Time
String
When I expect the new datetime attribute of the view to be also a Time or DateTime in the ruby world. Any ideas?
I also tried:
create view clowns as select p.name, p.born_at, CAST(datetime(p.born_at, '+' || '20' || ' minutes') as datetime) as twenty_after_born_at from people p;
With the same result.

Well, after more investigation, I found that:
MySQL works:
%w|pp rubygems active_record irb active_support date|.each {|lib| require lib}
ActiveRecord::Base.establish_connection(
:adapter => "mysql",
:username => "root",
:database => "test2"
)
ActiveRecord::Schema.define do
create_table :people, :force => true do |t|
t.column :name, :string
t.column :born_at, :datetime
end
execute "create view clowns as select p.name, p.born_at, (p.born_at + INTERVAL 20 MINUTE) as twenty_after_born_at from people p;"
end
class Person < ActiveRecord::Base
validates_presence_of :name
end
class Clown < ActiveRecord::Base
end
Person.create(:name => "John", :born_at => DateTime.now)
pp Person.all.first.born_at.class
pp Clown.all.first.born_at.class
pp Clown.all.first.twenty_after_born_at.class
Produces:
Time
Time
Time
Reading the sqlite3 adapter source code, I found out that it uses PRAGMA table_info(table_name) to get the type information, and that does not return the types for views:
sqlite> pragma table_info('people');
0|id|INTEGER|1||1
1|name|varchar(255)|0||0
2|born_at|datetime|0||0
sqlite> pragma table_info('clowns');
0|name|varchar(255)|0||0
1|born_at|datetime|0||0
2|twenty_after_born_at||0||0
Therefore it may be a limitation of the adapter or just a sqlite3's views limitation. I have opened a ticket for ActiveRecord:
Also, quoting this mail in sqlite-users:
RoR should be using the
sqlite3_column_type() API to determine
the type of the values returned from a
query. Other APIs like
sqlite3_column_decltype() and pragma
table_info are returning other
information, not the type of the
result value.

Well, basically there is no datatime type in SQLite as opposed to MySQL. In your example you explicitly define types for the table but do not specify types for the view. That might be the problem. Can not check it since I have never touched ruby.

Related

SQLite Creating Table Dynamically

I have a table in SQLite and when an entry is made in this table, I would like to create a new table for each entry in the first table. (I want to do this in Ruby On Rails, if that helps)
Here is an example to clarify what I am trying to achieve:
Assume there is a table: Campaigns
Campaigns
Campaign_ID, date, name
So if I make an entry:
01 06/12 FirstCampaign
is there a way to create a new table called: {Campaign_ID}_Page
i.e:
01_Page
(fields for this table go here)
Why do you need it?
I think it would be better to create a table Pages with a foreign key to your table Campaigns.
An example (I use Sequel):
require 'sequel'
DB = Sequel.sqlite
DB.create_table :Campaigns do
primary_key :id
column :campaign_id, :integer
column :date, :date
column :name, :string
end
DB.create_table :Pages do
primary_key :id
foreign_key :campaign_id, :Campaigns
column :text, :string
end
key = DB[:Campaigns].insert(:campaign_id => 01, :date=> Date.new(2012,1,1), :name => 'FirstCampaign')
DB[:Pages].insert(:campaign_id => key, :text => 'text for FirstCampaign')
key = DB[:Campaigns].insert(:campaign_id => 02, :date=> Date.new(2012,1,1), :name => 'SecondCampaign')
DB[:Pages].insert(:campaign_id => key, :text => 'text for SecndCampaign')
#All pages for 1st campaign
p DB[:Pages].filter(
:campaign_id => DB[:Campaigns].filter(:campaign_id => 1).first[:id]
).all
But to answer your question: You could try to use a model hook.
An example with Sequel:
require 'sequel'
DB = Sequel.sqlite
DB.create_table :Campaigns do
primary_key :id
column :campaign_id, :integer
column :date, :date
column :name, :string
end
class Campaign < Sequel::Model
def after_create
tabname = ("%05i_page" % self.campaign_id).to_sym
puts "Create table #{tabname}"
self.db.create_table( tabname ) do
foreign_key :campaign
end
end
end
p DB.table_exists?(:'01_page') #-> false, table does not exist
Campaign.create(:campaign_id => 01, :date=> Date.new(2012,1,1), :name => 'FirstCampaign')
p DB.table_exists?(:'00001_page') #-> true Table created
My example has no test, if the table already exist. If you really want to use it,

Using ActiveRecord inside another class

I'm trying to set up an IRC bot using ActiveRecord on the back end to handle all the data heavy lifting (probably overkill, but this is partly a learning experience for me :3)
The issue I'm running in to is that, after defining my database schema, later on in the same script when I try to reference the table I created, I get an error from the SQLite gem saying that it could not find the table.
Furthermore, my IDE (RubyMine) complains that it is "Unable to find the rails model for :notes association field"
Something tells me this would not be happening if I weren't constrained from operating as a class of the bot framework, but that is only a wild guess at this point.
What am I doing wrong here?
require 'cinch'
require 'active_record'
puts 'Memobox loaded'
class Memobox
include Cinch::Plugin
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => ':memory:'
)
ActiveRecord::Schema.define do
create_table :notes do |table|
table.column :id, :integer
table.column :timeset, :DateTime
table.column :sender, :string
table.column :recipient, :string
table.column :text, :string
end
end
class Note < ActiveRecord::Base
has_many :notes
end
match(/note.*/, :prefix => "?")
def execute(m)
Memobox::Note.create(
:timeset => (Time.new).ctime,
:sender => m.user.nick,
:text => m.message,
:recipient => (m.message).split("_").at(1)
)
end
end
Error:
C:/Ruby193/lib/ruby/gems/1.9.1/gems/activerecord-3.2.8/lib/active_record/connection_adapters/sqlite_adapter.rb:472:in `table_structure': Could not find table 'notes' (ActiveRecord::StatementInvalid)
You should replace this
class Note < ActiveRecord::Base
has_many :notes
end
with
class Note < ActiveRecord::Base
end
The descendants of ActiveRecord::Base class represents single row in a table, not a whole table.
So to find some note by id you need just to call Note.find(123), where 123 is the id of note record in db table.
Thanks to everyone for the clarification on the use of has_many and the syntax, but my problem ended up being the use of an in memory table instead of an on disk one. Once I changed line seven to say
:database => 'notes.db'
instead of
:database => ':memory:'
and removed the has_many declaration from the Notes class (I did try it without doing this and got a different error) , everything works :)

How can I reload the table schema in sequel?

Given I have the following migration:
Sequel.migration do
up do
alter_table :users do
add_column :is_admin, :default => false
end
# Sequel runs a DESCRIBE table statement, when the model is loaded.
# At this point, it does not know that users have a is_admin flag.
# So it fails.
#user = User.find(:email => "admin#fancy-startup.example")
#user.is_admin = true
#user.save!
end
end
Then sequel does not automatically reload the table structure (see comment inline).
I am using this ugly hack to work around it:
# deep magic begins here. If you remove a single line, it will
# break the migration.
User.db.schema("users", :reload => true)
User.instance_variable_set(:#db_schema, nil)
User.columns
User.new.respond_to?(:is_admin=)
sleep 1
Is there a better way?
Much simpler than your hack is this hack: (re)set the dataset to the table name:
User.set_dataset :users
Seen in action:
require 'sequel'
DB = Sequel.sqlite
DB.create_table :users do
primary_key :id
String :name
end
class User < Sequel::Model; end
User << { name:"Bob" }
DB.alter_table :users do
add_column :is_admin, :boolean, default:false
end
p User.first #=> #<User #values={:id=>1, :name=>"Bob", :is_admin=>false}>
p User.setter_methods #=> ["name="]
User.set_dataset :users # Make the magic happen
p User.setter_methods #=> ["name=", "is_admin="]
#user = User.first
#user.is_admin = true
#user.save
p User.first #=> #<User #values={:id=>1, :name=>"Bob", :is_admin=>true}>
Note that there is no Sequel::Model#save! method; I changed it to save so that it would work.

ActiveRecord and sqlite3 : find does not accept any condition?

I have a problem I can't figure out here. I'm writing a ruby script that deals with an sqllite database.
require 'rubygems'
require 'sqlite3'
require 'active_record'
ActiveRecord::Base.establish_connection(
:adapter => "sqlite3",
:database => "../database/my.db"
)
class KeyWord < ActiveRecord::Base
set_table_name "keywords"
end
# THIS STATEMENT WORKS (finds the first record, returns "ruby") :
KeyWord.find(1).keyval
# THOSE STATEMENTS RETURN NO RESULT :
KeyWord.find(:all, :conditions => {:keyval => "ruby"})
KeyWord.find_by_sql("SELECT * FROM keywords WHERE keyval='ruby'")
KeyWord.find_by_keyval("ruby")
This is how the table was created :
create_table :keywords do |table|
table.column :keyval, :text
end
Does anyone know where this could come from ?
Thanks,
R.
I see a couple of issues here.
I'm not sure why you're manually setting your table name.
ActiveRecord assumes that your model name is camelCased. So... AR
would, by default, search for a table called key_words. Why not just
go with that?
Pay attention to which version of active record you are
using. Passing in conditions is deprecated. You should be using the
.where syntax. So... you would need to do KeyWord.where(:keyval
=> 'ruby').first or end in .all for a collection of results.
If you are just fooling around, you can use sqlite3 in memory.
ActiveRecord::Base.establish_connection( adapter: 'sqlite3',
database: ":memory:")
Also don't forget to define your schema!
Here is full code w/ more modern syntax.
require 'rubygems'
require 'sqlite3'
require 'active_record'
ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )
ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define(version: 2) do
create_table :key_words do |t|
t.text :keyval
end
end
class KeyWord < ActiveRecord::Base
end
Keyword.create!(:keyval => 'ruby')
# THESE STATEMENTS WORK:
KeyWord.find(1).keyval
KeyWord.where(:keyval => 'ruby').first
KeyWord.where(:keyval => 'ruby').all
KeyWord.find_by_sql("SELECT * FROM key_words WHERE keyval='ruby'")
KeyWord.find_by_keyval("ruby")

Pony and Sequel associations conflict?

I've run into an issue when using Pony and Sequel in a Sinatra application.
Without Pony everything goes just fine, but just requiring Pony sequel's associations break.
Here's my models for a blog:
class Post < Sequel::Model
one_to_many :comments, :order => :date.asc(), :conditions => {:approved => 1}
set_schema do
primary_key :id
varchar :title
varchar :text
varchar :category
varchar :status
datetime :date
varchar :link
end
end
class Comment < Sequel::Model
plugin :validation_helpers
many_to_one :posts
attr_accessor :ip, :user_agent, :referrer, :permalink
set_schema do
primary_key :id
integer :post_id
varchar :author
varchar :comment
DateTime :date
varchar :email
varchar :url
varchar :approved
end
Then I call them like this in a route
post '/:link' do
#post = Post[:link=>params[:link]]
params[:comment].merge!( {
:ip => request.ip.to_s,
:user_agent => request.env['HTTP_USER_AGENT'].to_s,
:referrer => request.env['REFERER'].to_s,
:permalink => request.env['REFERER'].to_s
} )
begin
#comment = Comment.create params[:comment]
#post.add_comment #comment
rescue
#message = $!
end
#title = #post.title
haml :posts
end
I don't even have to call pony somewhere, just requiring it #post.add_comment #comment fails. It says
NoMethodError - undefined method `_add_comments' for #<Post:0x102b09890>:
/Library/Ruby/Gems/1.8/gems/sequel-3.21.0/lib/sequel/model/associations.rb:1078:in `send'
/Library/Ruby/Gems/1.8/gems/sequel-3.21.0/lib/sequel/model/associations.rb:1078:in `add_associated_object'
/Library/Ruby/Gems/1.8/gems/sequel-3.21.0/lib/sequel/model/associations.rb:743:in `add_comment'
Seems to me like a conflict with send? I don't even know how to start to debug it.
This is caused by an ActiveSupport issue, believe it or not. You should drop down to ActiveSupport 3.0.3 or manually require the default ActiveSupport inflections via:
require 'active_support/inflections'
Basically, after 3.0.3, ActiveSupport made it possible to load the inflector without the default inflections, which results in broken singularize and pluralize methods. The mail gem, which I'm guessing pony uses, is one of libraries that is known to be broken by this change.
The Rails developers apparently do not consider this a bug in ActiveSupport, but a bug in the libraries that use ActiveSupport.

Resources