Set Primary Key value 0 and auto increment on Migration PostgreSQL - ruby-on-rails

I have a model with 2 fields => :name and :age
I need to do a Migration that add a column :position which needs auto increment and start with 0 (zero).
I tried these way:
class AddPosition < ActiveRecord::Migration
def up
add_column :clientes, :position, :integer, :default => 0, :null => false
execute "ALTER TABLE clientes ADD PRIMARY KEY (position);"
end
But it doesn't work because it not auto increment. If I try to use primary key as type:
class AddPosition < ActiveRecord::Migration
def up
add_column :clientes, :position, :primary_key, :default => 0, :null => false
end
end
rake db:migrate don't run because multiple values.
Anyone could explain a way to have zeros and autoincrement on Primary Key w/ Rails 3.2?

Here's how you can set up auto increment column in PostgreSQL:
# in migration:
def up
execute <<-SQL
CREATE SEQUENCE clients_position_seq START WITH 0 MINVALUE 0;
ALTER TABLE clients ADD COLUMN position INTEGER NOT NULL DEFAULT nextval('clients_position_seq');
SQL
end
But unfortunately it may not be what you need. The above would work if you'd insert values into clients table with SQL like this: INSERT INTO clients(name, age) VALUES('Joe', 21), and rails doesn't work that way.
The first problem is that rails expects primary key to be called id. And while you can override this convention, it would cause more problems than it would solve. If you want to bind position to primary key value, better option is to add virtual attribute to your model, a method like this:
def position
id.nil? ? nil : id - 1
end
But let's suppose you already have conventional primary key id and want to add position field so that you can reorder clients after they have been created, without touching their ids (which is always a good idea). Here comes the second problem: rails won't recognize and respect DEFAULT nextval('clients_position_seq'), i.e. it won't pull values from PG backed sequence and would try to put NULL in position by default.
I'd like to suggest looking at acts_as_list gem as better option. This would make DB sequence manipulations unnecessary. Unfortunately it uses 1 as initial value for position but that can be cured by setting custom name for list position field and defining method as I showed above.

Related

Increase ActiveModel ID Range to 8 byte

I've been digging around to see how I could have all my newly and subsequent Model id's to have a limit of 8 byte. Answers show how to when adding a new table column; I want whenever I create a new Model, it would automatically has a limit of 8 byte. Possible?
When creating a new model, I get:
ActiveModel::RangeError: 36565651767 is out of range for ActiveModel::Type::Integer with limit 4
Where to change this limit from 4 to 8?
A possible duplicate but since there will be errors:
you can't redefine the primary key column 'id'. To define a custom primary key, pass { id: false } to create_table.
Which means your table should look like this:
class MyModels < ActiveRecord::Migration[5.0]
def change
create_table :my_models, {id: false } do |t|
t.column :id, limit: 8
...
end
end
end

Ruby on Rails ignoring integer limit

I need to index a table of users using an externally sourced id, which is a 64-bit integer. Rails is perfectly capable of storing such a number, unless it's the primary key it seems. I have the following migration:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users, :id => false do |t|
t.integer :id, limit: 8
t.string :name
t.timestamps null: false
end
end
end
The migration works fine, no errors reported, but when I attempt to seed it with a 64-bit integer, I'm told off by this:
RangeError: 76561198054432981 is out of range for ActiveRecord::Type::Integer with limit 4
Obviously Rails is ignoring the limit field, so long as it's the primary key/the :id field? How should I go about dealing with this?
For what it's worth I'm using sqlite3 (default), but to my knowledge, sqlite is perfectly capable of storing 64-bit integers.
Here's the table_info from sqlite:
0|id|integer(8)|0||0
1|name|varchar|0||0
2|created_at|datetime|1||0
3|updated_at|datetime|1||0
The limit value you gave is correct; it corresponds to BIGINT type
Make sure your migration is applied; open you database in some CLI or GUI software and verify the col-type
Addition:
Changing a column's length or datatype in a migration will invalidate the column as a primary key. Rather, creating an initializer that overrides the site's default primary key datatype should provide the behavior you're looking to implement:
# config/initializers/change_primary_key_datatype.rb
require 'active_record/connection_adapters/postgresql_adapter'
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:primary_key] = "bigserial primary key"
This is what we would do for PG database; This is possible because of
however in the code base of SQLite there is

How to enforce uniqueness of an entire row?

I've seen other SO questions like - How do you validate uniqueness of a pair of ids in Ruby on Rails? - which describes adding a scoped parameter to enforce uniqueness of a key pair, i.e. (from the answer)
validates_uniqueness_of :user_id, :scope => [:question_id]
My question is how do you do this kind of validation for an entire row of data?
In my case, I have five columns and the data should only be rejected if all five are the same. This data is not user entered and the table is essentially a join table (no id or timestamps).
My current thought is to search for a record with all of the column values and only create if the query returns nil but this seems like a bad work around. Is there an easier 'rails way' to do this?
You'll need to create a custom validator (http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations):
class TotallyUniqueValidator < ActiveModel::Validator
def validate(record)
if record.attributes_for_uniqueness.values.uniq.size == 1
record.errors[:base] << 'All fields must be unique!'
end
end
end
class User
validates_with TotallyUniqueValidator
def attributes_for_uniqueness
attributes.except :created_at, :updated_at, :id
end
end
The important line here is:
if record.attributes_for_uniqueness.values.uniq.size == 1
This will grab a hash of all the attributes you want to check for uniqueness (in this case everything except id and timestamps) and converts it to an array of just the values, then calls uniq on it which returns only uniq values and if the size is 1 then they were all the same value.
Update based on your comment that your table doesn't have an id or timestamps:
You can then simply do:
if record.attributes.except(:id).values.uniq.size == 1
...because I'm pretty sure it still has an id unless you're sure it doesn't then just remove the except part.
You can add a unique index to the table in a migration:
add_index :widgets, [:column1, :column2, :column3, :column4, :column5], unique: true
The resulting index will require that each combination of the 5 columns must be unique.

Rails migrations - change_column with type conversion

I already google'd aroung a little bit and seems there's no satisfying answer for my problem.
I have a table with column of type string.
I'd like to run following migration:
class ChangeColumnToBoolean < ActiveRecord::Migration
def up
change_column :users, :smoking, :boolean
end
end
When I run this I get following error
PG::Error: ERROR: column "smoking" cannot be cast automatically to type boolean
HINT: Specify a USING expression to perform the conversion.
: ALTER TABLE "users" ALTER COLUMN "smoking" TYPE boolean
I know I can perform this migration using pure SQL but still it would be nicer if I could do it with Rails. I went through Rails code and seems theres no such possibility, but maybe someone knows a way?
I'm not interested in:
- pure SQL
- dropping the column
- creating another column, converting data, dropping original and then renaming
If your strings in smoking column are already valid boolean values, the following statement will change the column type without losing data:
change_column :users, :smoking, 'boolean USING CAST(smoking AS boolean)'
Similarly, you can use this statement to cast columns to integer:
change_column :table_name, :column_name, 'integer USING CAST(column_name AS integer)'
I am using Postgres. Not sure whether this solution works for other databases.
Not all databases allow changing of column type, the generally taken approach is to add a new column of the desired type, bring any data across, remove the old column and rename the new one.
add_column :users, :smoking_tmp, :boolean
User.reset_column_information # make the new column available to model methods
User.all.each do |user|
user.smoking_tmp = user.smoking == 1 ? true : false # If smoking was an int, for example
user.save
end
# OR as an update_all call, set a default of false on the new column then update all to true if appropriate.
User.where(smoking: 1).update_all(smoking_tmp: true)
remove_column :users, :smoking
rename_column :users, :smoking_tmp, :smoking
So right for boolean in postgres:
change_column :table_name, :field,'boolean USING (CASE field WHEN \'your any string as true\' THEN \'t\'::boolean ELSE \'f\'::boolean END)'
and you may add some more WHEN - THEN condition in your expression
For other database servers, the expression will be constructed based on the syntax for your database server, but the principle is the same. Only manual conversion algorithm, entirely without SQL there is not enough unfortunately.
The syntax change_column :table, :field, 'boolean USING CAST(field AS boolean)' is suitable only if the contents of the field something like: true / false / null
Since I'm using Postgres, I went with SQL solution for now.
Query used:
execute 'ALTER TABLE "users" ALTER COLUMN "smoking" TYPE boolean USING CASE WHEN "flatshare"=\'true\' THEN \'t\'::boolean ELSE \'f\'::boolean END'
It works only if one has a field filled with true/false strings (such as default radio button collection helper with forced boolean type would generate)

How to add sequences to a migration and use them in a model?

I want to have a "Customer" Model with a normal primary key and another column to store a custom "Customer Number". In addition, I want the db to handle default Customer Numbers. I think, defining a sequence is the best way to do that. I use PostgreSQL. Have a look at my migration:
class CreateAccountsCustomers < ActiveRecord::Migration
def up
say "Creating sequenze for customer number starting at 1002"
execute 'CREATE SEQUENCE customer_no_seq START 1002;'
create_table :accounts_customers do |t|
t.string :type
t.integer :customer_no, :unique => true
t.integer :salutation, :limit => 1
t.string :cp_name_1
t.string :cp_name_2
t.string :cp_name_3
t.string :cp_name_4
t.string :name_first, :limit => 55
t.string :name_last, :limit => 55
t.timestamps
end
say "Adding NEXTVAL('customer_no_seq') to column cust_id"
execute "ALTER TABLE accounts_customers ALTER COLUMN customer_no SET DEFAULT NEXTVAL('customer_no_seq');"
end
def down
drop_table :accounts_customers
execute 'DROP SEQUENCE IF EXISTS customer_no_seq;'
end
end
If you know a better "rails-like" approach to add sequences, would be awesome to let me know.
Now, if I do something like
cust = Accounts::Customer.new
cust.save
the field customer_no is not pre filled with the next value of the sequence (should be 1002).
Do you know a good way to integrate sequences? Or is there a good plugin?
Cheers to all answers!
I have no suggestions for a more 'rails way' of handling custom sequences, but I can tell you why the customer_no field appears not to be being populated after a save.
When ActiveRecord saves a new record, the SQL statement will only return the ID of the new record, not all of its fields, you can see where this happens in the current rails source here https://github.com/rails/rails/blob/cf013a62686b5156336d57d57cb12e9e17b5d462/activerecord/lib/active_record/persistence.rb#L313
In order to see the value you will need to reload the object...
cust = Accounts::Customer.new
cust.save
cust.reload
If you always want to do this, consider adding an after_create hook in to your model class...
class Accounts::Customer < ActiveRecord::Base
after_create :reload
end
I believe that roboles answer is not correct.
I tried to implement this on my application (exactly the same env: RoR+PostgreSQL), and I found out that when save is issued on RoR with the object having empty attributes, it tries to perform an INSERT on the database mentioning that all VALUES shall be set to NULL. The problem is the way PostgreSQL handles NULLs: in this case, the new row will be created but with all values empty, i.e. the DEFAULT will be ignored. If save only wrote on the INSERT statement attributes filled on RoR, this would work fine.
In other words, and focusing only on the type and customer_no attribute mentioned above, this is the way PostgreSQL behaves:
SITUATION 1:
INSERT INTO accounts_customers (type, customer_no) VALUES (NULL, NULL);
(this is how Rails' save works)
Result: a new row with empty type and empty customer_no
SITUATION 2:
INSERT INTO accounts_customers (type) VALUES (NULL);
Result: a new row with empty type and customer_no filled with the sequence's NEXTVAL
I have a thread going on about this, check it out at:
Ruby on Rails+PostgreSQL: usage of custom sequences
I faced a similar problem, but I also put :null => false on the field hopping that it will be auto-populated with nextval.
Well, in my case AR was still trying to insert NULL if no attribute was supplied in the request, and this resulted in an exception for not-null constraint violation.
Here's my workaround. I just deleted this attribute key from #attributes and #changed_attributes and in this case postgres correctly put the expected sequence nextval.
I've put this in the model:
before_save do
if (#attributes["customer_no"].nil? || #attributes["customer_no"].to_i == 0)
#attributes.delete("customer_no")
#changed_attributes.delete("customer_no")
end
end
Rails 3.2 / Postgres 9.1
If you're using PostgreSQL, check out the gem I wrote, pg_sequencer:
https://github.com/code42/pg_sequencer
It provides a DSL for creating, dropping and altering sequences in ActiveRecord migrations.

Resources