Default datetime with Ecto & Elixir - postgresql

I've just started working Elixir & Phoenix today, i am trying to add Ecto as a mapper, but i'm having some trouble using time.
This is my model.
schema "users" do
field :name, :string
field :email, :string
field :created_at, :datetime, default: Ecto.DateTime.local
field :updated_at, :datetime, default: Ecto.DateTime.local
end
I'm trying to set the created_at and updated_at per default, but when i try to compile this, i get the following error.
== Compilation error on file web/models/user.ex ==
** (ArgumentError) invalid default argument `%Ecto.DateTime{day: 13, hour: 19, min: 47, month: 2, sec: 12, year: 2015}` for `:datetime`
lib/ecto/schema.ex:687: Ecto.Schema.check_default!/2
lib/ecto/schema.ex:522: Ecto.Schema.__field__/4
web/models/board.ex:9: (module)
(stdlib) erl_eval.erl:657: :erl_eval.do_apply/6
There is not much help to get in the documentation, what would be the correct way to do this?

Defaults fields names are :inserted_at and :updated_at but you can merge with your own field names, passing a keyword list
schema "users" do
field :name, :string
field :email, :string
timestamps([{:inserted_at,:created_at}])
end

:datetime is the native Postgres data type for, well a datetime; this data type maps to a two-elements Elixir tuple ({{yy, mm, dd}, {hh, mm, ss}}). An %Ecto.DateTime{} struct is not a two-elements tuple, hence the compilation error.
You may want to set the type of your fields to Ecto.DateTime, it should all work seamlessly.
Here is the relevant documentation about primitive types and non-primitive types.
PS you may also want to have a look at Ecto.Schema.timestamps/1, which is macro that expands to basically what you wrote manually (it adds the created_at and updated_at fields and it let's you choose what type they should be, defaulting to Ecto.DateTime):
schema "users" do
field :name, :string
field :email, :string
timestamps
end

You could also consider having the default not be in the schema, but in the migration: "created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP"

Related

Indexing of JSONB embedded Ecto2 models in Postgres 9.4+

I'm not clear on how to index embedded structs stored as JSONB with Ecto2/Postgres 9.4+
I have a schema with two embedded structs using embeds_one and embeds_many. They are ecto :map fields represented in Postgres as JSONB. I am wondering how I can be sure they are indexed (using Gin?) for speedy queries? I am not sure if this happens automagically, if I need to add an index to my migration or if I need to do it manually using psql etc..
Just looking for clarification on how this works.
Thanks!
defmodule App.Repo.Migrations.CreateClient
def change do
create table(:clients) do
add :name, :string
add :settings, :map
add :roles, {:array, :map}, default: []
timestamps()
end
// This works for normal schema/model fields
create index(:clients, [:name], unique: true, using: :gin)
// BUT CAN I INDEX MY EMBEDS HERE?
// GUESS:
create index(:clients, [:settings], using: :gin)
end
end
defmodule App.Client do
schema "client" do
field :name, :string
embeds_one :settings, Settings // single fixed schema "Settings" model
embeds_many :roles, Role // array of "Role" models
end
end
defmodule Settings do
use Ecto.Model
embedded_schema do // ALSO
field :name, :string // are types relevant?
field :x_count, :integer // stored as strings (INDEXED?)
field :is_active, :boolean // deserialized via cast?
end
end
defmodule Role do
use Ecto.Model
embedded_schema do
field :token
field :display_english
field :display_spanish
end
end
I think you just need to add this:
create index(:clients, [:name], unique: true, using: :gin)
to your migration file.
Or if the index sql statement is gonna be complicated, you could do it with execute so it would be something like this:
execute("CREATE INDEX clients_name_index ON clients USING GIN (name)")
I have not tested it but I believe it should work.

Mongoid/MongoDB: Order a query by the value of an embedded document?

I am attempting to order the results of a query by the value of a specific embedded document, but even with what seems to be a valid set of options and using the $elemMatch operator, my results are coming back in natural order.
My model is composed of Cards, which embeds_many :card_attributes, which in turn reference a specific CardAttributeField and contain an Integer value. I would like to be able to order a collection of Cards by that value.
I am able to isolate a collection of Cards which have a CardAttribute referencing a specific CardAttributeField like this:
cards = Card.where(:card_attributes.elem_match => {
:card_attribute_field_id => card_attribute_field.id
})
If I knew the order in which the card_attributes were set, I could use MongoDB array notation, like this:
cards.order_by(['card_attributes.0.value', :asc])
This does deliver my expected results in test scenarios, but it won't work in the real world.
After much messing around, I found a syntax which I thought would allow me to match a field without using array notation:
cards.asc(:'card_attributes.value'.elem_match => {
:card_attribute_field_id => card_attribute_field.id
})
This produced a set of options on the resulting Mongoid::Criteria which looked like:
{:sort=>{"{#<Origin::Key:0x2b897548 #expanded=nil, #operator=\"$elemMatch\", #name=:\"card_attributes.value\", #strategy=:__override__, #block=nil>=>{:card_attribute_field_id=>\"54c6c6fe2617f55611000068\"}}"=>1}}
However, the results here came back in the same order regardless or whether I called asc() or desc().
Is there any way to do what I'm after? Am I taking the wrong approach, or do I have a mistake in my implementation? Thanks.
Simplified, my model is:
class Card
include Mongoid::Document
# various other fields
has_many :card_attribute_fields
embeds_many :card_attributes do
def for_attribute_field card_attribute_field
where(:card_attribute_field_id => card_attribute_field.id)
end
end
end
class CardAttributeField
include Mongoid::Document
belongs_to :card
field :name, type: String
field :default_value, type: String
field :description, type: String
end
class CardAttribute
include Mongoid::Document
embedded_in :card
field :card_attribute_field_id, type: Moped::BSON::ObjectId
field :value, type: Integer
end

Modify Devise to support UUID primary key

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,
...

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.