Modify Devise to support UUID primary key - postgresql

I want to modify Devise to make it work with a users table with a UUID primary key with PostgreSQL.
Here is the migration:
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table :users, id: false do |t|
t.uuid :uuid, null: false
# ...
end
change_table :users do |t|
t.index :uuid, unique: true
# ...
end
end
def migrate(direction)
super
if direction == :up
# This is only necessary because the following does not work:
# t.uuid :uuid, primary: true, null: false
execute "ALTER TABLE users ADD PRIMARY KEY (uuid);"
end
end
end
Here is the User model:
class User < ActiveRecord::Base
primary_key = :uuid
devise :database_authenticatable, :recoverable, :registerable,
:rememberable, :trackable, :validatable
validates :uuid, presence: true
before_validation :ensure_uuid
def ensure_uuid; self.uuid ||= SecureRandom.uuid end
end
Here is the error:
PG::Error: ERROR: operator does not exist: uuid = integer
LINE 1: ...ECT "users".* FROM "users" WHERE "users"."uuid" = 1 ORDER...
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "users".* FROM "users" WHERE "users"."uuid" = 1 ORDER BY "users"."uuid" ASC LIMIT 1
Extracted source (around line #5):
1 .navbar-inner
2 .container
3 = a_nav_tag "App", root_path
4 - if user_signed_in?
5 %ul.nav.pull-right
6 %li.dropdown#user_menu
7 %a.dropdown-toggle(data-toggle="dropdown" href="#")
As you can see above, user_signed_in? is broken. I expect there are several changes needed to move from a 'normal' auto-incrementing ID to a UUID.
For now, I'm just posting the question. I'll take a swing at this later today. If you happen to know how to do this -- or know of a Devise fork, I'd appreciate it.

I've done this in Rails 4 simply by making the id column a uuid data type when creating the table, and no other configuration changes whatsoever. ie. do not create a column named 'uuid', just change the type of the 'id' column to uuid.

Just clear your browser's cookie for the web app (in my case, localhost). The error above is caused because the session was retaining the old user primary key, 1.
After that, things work in my testing. I hope this isn't just luck, it would be a good design if Devise was agnostic about the primary key. (In Devise's code, I saw no use of .id except in some tests.)

2020 answer:
when creating the users table, set the ID as uuid
def change
enable_extension 'pgcrypto' # needed if not already enabled
create_table :users, id: :uuid do |t|
t.string :email,
...

Related

How to set null when reference table row is deleted, Ecto.Migration references/2

I have two tables devices and devices_type, I want to set null in devices.type_id when original row from devices_types is deleted.
#MyApp.Repo.Migrations.Devices
create table(:devices) do
...
add :type_id, references(:device_type, on_delete: :nothing)
...
end
#MyApp.Repo.Migrations.DeviceType
create table(:device_type) do
add :name, :string, comment: "Device type name"
end
Reference to documentation references(table, opts \ []), I use on_delete: :nilify_all option
#MyApp.Repo.Migrations.Devices
create table(:devices) do
...
add :type_id, references(:device_type, on_delete: :nilify_all)
...
end
see too, check in options on_delete:
https://hexdocs.pm/ecto/Ecto.Schema.html#has_many/3

On conflict not updating back to default values

I have a users table like this
create table(:users) do
add(:email, :citext)
add(:email_confirmation_token, :string)
add(:email_confirmed_at, :utc_datetime)
add(:deleted_at, :utc_datetime)
timestamps()
end
and I have User schema as
schema "users" do
field(:email, :string)
field(:deleted_at, :utc_datetime)
field(:email_confirmed_at, :utc_datetime)
field(:email_confirmation_token, :string, default: ExApi.Utils.Random.string())
timestamps()
end
Now if I insert record with on_conflict option I want to reset deleted_at but deleted_at is never passed to changeset. I used following line of code
attrs = %{"email" => "tanweer.shahzaad#gmail.com", "deleted_at" => nil}
cs = %User{} |> User.changeset(attrs)
ExApi.Repo.insert(cs, on_conflict: :replace_all, conflict_target: :email)
Any workaround which will reset deleted_at will be very much appreciated.
Problem: if deleted_at is already set, I want to reset it back to nil on insert

How do I tell my migration what column type to use?

I'm using Rails 5 and PostGres 9.5. I have created this migration
class CreateSearchCodeTable < ActiveRecord::Migration[5.0]
def change
create_table :search_codes do |t|
t.string :code
t.references :address, index: true, foreign_key: true, on_delete: :cascade
t.index ["code"], name: "index_search_codes_on_code", unique: true, using: :btree
end
end
end
The ID column of the address table is not an integer though. Maybe for that reason, I get the below error when I run the migration
== 20171011202623 CreateSearchCodeTable: migrating ============================
-- create_table(:search_codes)
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:
PG::DatatypeMismatch: ERROR: foreign key constraint "fk_rails_6bd9792e3b" cannot be implemented
DETAIL: Key columns "address_id" and "id" are of incompatible types: integer and character varying.
: CREATE TABLE "search_codes" ("id" serial primary key, "code" character varying, "address_id" integer, CONSTRAINT "fk_rails_6bd9792e3b"
FOREIGN KEY ("address_id")
REFERENCES "addresses" ("id")
)
How do I instruct my migration to create my column with the same type as the referenced column?
t.references delegates job to add_reference and this one accepts :type parameter. Judging from this, you should be able to do
t.references :address, type: :string, index: true, foreign_key: true, on_delete: :cascade
^^^^^^^^^^^^^
Just tested this on a toy sqlite3-based project, it worked. Should "just work" on pg too.
How do I instruct my migration to create my column with the same type as the referenced column?
If you meant to tell it to infer the type of the other column and have type of this one match it, then this is likely not possible. But you can always specify type explicitly.

How to: Single Table Inheritance in DataMapper?

I'm learning Sinatra (1.3.2) and chose to use DataMapper (1.2.0) as ORM and an in-memory SQLite (1.3.6) DB to start.
Two models, Books and Downloads, are sharing most attributes, so I looked into declaring a model for STI (Single Table Inheritance) in DataMapper. Reading the docs, this seems a piece of cake thanks to Types::Discriminator.
I abstracted all common ones into DownloadableResource:
class DownloadableResource
include DataMapper::Resource
property :id, Serial
property :created_at, DateTime
property :modified_at, DateTime
property :active, Boolean, default: true
property :position, Integer
property :title, String, required: true
property :url, URI, required: true
property :description, Text, required: true
property :type, Discriminator
end
Following the example, I thought it's just as easy as specifying what needs to be extended:
class Book < DownloadableResource
property :cover_url, URI
property :authors, String, required: true, length: 255
end
and
class Download < DownloadableResource
property :icon_url, URI
end
but this was giving me the following error:
DataObjects::SyntaxError: duplicate column name: id (code: 1, sql state: , query: ALTER TABLE "downloadable_resources" ADD COLUMN "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, uri: sqlite3::memory:?scheme=sqlite&user=&password=&host=&port=&query=&fragment=&adapter=sqlite3&path=:memory:)
while removing the id generated another (obvious) error:
DataMapper::IncompleteModelError: DownloadableResource must have a key to be valid
I got around this by adding include DataMapper::Resource to both Book and Download, and then Book needed a key to be valid, now looking like this:
class Book < DownloadableResource
include DataMapper::Resource
property :id, Serial
property :cover_url, URI
property :authors, String, required: true, length: 255
end
Same goes for Download, but now the issue is:
DataObjects::SyntaxError: duplicate column name: id (code: 1, sql state: , query: ALTER TABLE "books" ADD COLUMN "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, uri: sqlite3::memory:?scheme=sqlite&user=&password=&host=&port=&query=&fragment=&adapter=sqlite3&path=:memory:)
Starting to feel like I'm going in circles, what's the proper way to implement Single Table Inheritance in DataMapper?
PS: I have looked at
DataMapper - Single Table Inheritance and
Ruby Datamapper table inheritance with associations
but I still have this problem.
I would recommend this approach:
module DownloadableResource
def self.included base
base.class_eval do
include DataMapper::Resource
property :created_at, DateTime
property :modified_at, DateTime
property :active, base::Boolean, default: true
property :position, Integer
property :title, String, required: true
property :url, base::URI, required: true
property :description, base::Text, required: true
property :type, base::Discriminator
end
end
end
class Book
include DownloadableResource
property :id, Serial
# other properties
end
class Download
include DownloadableResource
property :id, Serial
# other properties
end

`execute_non_query': Cannot add a NOT NULL column with default value NULL (DataObjects::SyntaxError)

class User
include DataMapper::Resource
property :id, Serial
property :name, String
property :email, String
has n, :records
end
class Project
include DataMapper::Resource
property :id, Serial
property :name, String
has n, :records ?????
end
#
class Record
# SPEND_REGEX = /^[0-9]{1}:[0-5]{1}[0-9]{1}$/
include DataMapper::Resource
property :id, Serial
property :reporting_type, String
property :spend_time, String
belongs_to :user
belongs_to :project ????
end
DataMapper.auto_upgrade!
With ??? I marked relation that throws an error "`execute_non_query': Cannot add a NOT NULL column with default value NULL (DataObjects::SyntaxError)
"
How to define 2 has many relationships to one model in datamapper?
By default, your belongs_to relationships are required. I assume you already have Record entries in your database. The auto_upgrade is trying to add the new field for the association, and by default it marks that column as NOT NULL. However, for all the existing records, that value will be NULL.
To get around this, do one of the following:
Do an auto_migrate instead of auto_upgrade. This will blow away your data, but
will allow you to add the relationship columns without it choking on
NULL values.
Make the associations optional with :required => false. This will allow NULLs in the database. Next, go in and set those fields to the appropriate values. Lastly, modify the database table column to be NOT NULL.