TinyTds::Error: Cannot insert the value NULL into column 'ID' - ruby-on-rails

My Ruby on Rails system is moving from Oracle to Microsoft SQL Server 2012. The back end database has already been converted by a third part from Oracle to Microsoft SQL Server. I have no control over the schema structure. This cannot be changed.
Using activerecord-sqlserver-adapter, tiny_tds and Freetds I can connect to the new database. Most DB requests work well.
However, many of the tables have ID primary keys that are auto incremented by SQL Server, in other words, PK IDENTITY(1,1) columns. The same code that worked with Oracle connections fails under SQL Server with the following error:
TinyTds::Error: Cannot insert the value NULL into column 'ID'
I know why it is doing this as the ID column is indeed a primary_key, and IDENTITY(1,1) column and includes the NOT NULL restriction. That is fine.
Inserting into the table works fine if using raw SQL execute statements where of course I exclude the ID column from the INSERT statement.
However I have spent days googling and I cannot find a way of telling Ruby on Rails not to try and save the ID column.
So I have an instance of a class, #book, that contains
Library::Book(id: integer, title: string, isbn: string ...)
When I do a #book.save! it generates the error above.
#book = Library::Book.new( .. )
:
:
#book.save!
TinyTds::Error: Cannot insert the value NULL into column 'ID'
Rather that resort to bare metal SQL, how do I do things more Railsy and tell it I want to save the record but not try and save the ID field as it is auto incremented? So effectively I am trying to save
Library::Book(title: string, isbn: string ...) if a new insert entry
or
Library::Book(id: integer, title: string, isbn: string ...) if trying to update an entry.
Due to imposed restrictions I am using:
Ruby 2.3.3p222
Rails 4.0.13
activerecord (4.0.13)
activerecord-sqlserver-adapter (4.0.4)
tiny_tds (1.0.2)
Freetds 1.00.27

You can use ActiveRecord::Relation#find_or_initialize_by. This assumes that you have enough attributes known at the time to uniquely identify the record.
That will solve the last part of your question. But it sounds like the id column is not set to auto-increment. You need to set that so when Rails sends a null id the DB will set it properly.
Update: To make the column auto-increment you will need to run a migration. You can then do:
Alter table books modify column id int(11) auto_increment;
Update: If we cannot modify the database we can do:
class Book
private
def create_or_update(*args, &block)
self.id ||= Book.maximum(:id) + 1
super
rescue ActiveRecord::RecordNotUnique
self.id = nil
retry
end
end
It's pretty janky, and I would highly recommend updating the column if possible.

Related

How to return generated columns in Rails ActiveRecord?

We'd like to introduce a trigger to set a value on a column, in a Rails 6 application. The table looks like this (PostgreSQL 9.6):
CREATE TABLE foo(
id bigserial primary key
, sku text not null unique
-- ...
);
On create, our code sets a prefix on SKU:
class Foo < ApplicationRecord
before_create :set_sku_prefix
private
def set_sku_prefix
self[:sku] = "AAA-"
end
end
When the trigger fires, it finishes setting the record up:
CREATE OR REPLACE FUNCTION generate_sku() RETURNS trigger AS $$
BEGIN
-- Simplified, real trigger handles ID already being set,
-- and other edge cases related to the length of the returned value.
NEW.id = nextval('public.foos_id_seq');
NEW.sku = NEW.sku || RIGHT('000000', NEW.id, 6);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE TRIGGER generate_sku
BEFORE INSERT OR UPDATE
ON foos
FOR EACH ROW
WHEN (RIGHT(NEW.sku, 1) = '-')
EXECUTE PROCEDURE generate_sku();
When ActiveRecord generates the SQL statement to do the insert, it does this:
INSERT INTO foos(sku) VALUES ('AAA-') RETURNING id
I'd like to change that last statement so that it also returns the SKU, and have ActiveRecord update the in-memory attribute value to what the database generated:
INSERT INTO foos(sku) VALUES ('AAA-') RETURNING id, sku
In the end, I'd like the following to be true:
foo = Foo.create!
if foo.sku == ("AAA-%06d" % [foo.id])
puts "All good!"
else
puts "Oops, wrong"
end
I have read most of the ActiveRecord API docs, and the Ruby on Rails Guides, to no avail. Searching on the general web also failed me. I searched for "rails activerecord trigger returning", with nothing that was really related to what I need.
Is it possible to make ActiveRecord read more of the written row and reload specific attributes?
The reason we're doing this is to avoid a double-write on INSERT. The existing code saves, then uses the record's ID to set the SKU, in Ruby-land, then writes the column back to the DB. This means 2 DB roundtrips for the same operation. The SKU column is also NULL-allowed, which I don't really like. We'd like to save a roundtrip to the DB, and enforce NOT NULL on the SKU column. If we have to reload the record, this will defeat the purpose of using a database trigger.

Oracle sequence not updated when using rails seeds file

Here's a part of a seeds file I have
Country.create(id: 1, name: 'Turkey', 'description' : 'Gobble gobble')
Country.create(id: 2, name: 'Hungary', 'description' : 'Blah blah blah')
There are two records for these in the countries table after this has run.
So now through the web interface or via an API I try to create a new country and I get the following error from Oracle:
OCIError: ORA-00001: unique constraint
The constraint is on the primary key. After seeding, the next two records it attempts to create with id's 1, 2 but they already exist.
I don't understand why when creating the Country by seeding with the same code as in the controller/model through an API or web interface I get a different result.
If I alter the sequence through SQLDeveloper with the following snippet, everything works fine:
ALTER SEQUENCE countries_seq INCREMENT BY 100;
select countries_seq from dual;
ALTER SEQUENCE countries_seq INCREMENT BY 1;
Note this is happening for all models, I'm just using Country as an example.
I'm using
Ruby 2.1.0
Rails 4.0.5
oracle-enhanced 1.5.5 gem
ruby-oci8 2.1.7 gem
EDIT
If I don't put id in the create arguments, everything works fine.
Country.create(name: 'Turkey', 'description' : 'Gobble gobble')
Country.create(name: 'Hungary', 'description' : 'Blah blah blah')
It'd be great to know why though...
I don't know what Country.create does, most probably: If you don't assign an id yourself it will be set by the create function or database side by a trigger. You could check the tabledefinition for a trigger.

Rails / ActiveRecord: How to quote protected column name using #select in custom query?

Running Rails 4.0.13 with TinyTDS connected to Microsoft SQL Server 2012, I'm trying to run the following query:
sql = Model.where(:foo => bar).select(:open, :high, :low, :close).to_sql
Model.connection.execute(sql)
The problem is, the generated sql is
"SELECT open, high, low, close FROM [models]"
Which gives me an error as the column names open and close are protected.
TinyTds::Error: Incorrect syntax near the keyword 'open'
If I use #pluck, I can see the correct SQL is generated (with column names escaped):
"SELECT [models].[open], [models].[high], [models].[low], [models].[close] FROM [models]"
However, this produces an array, which is not what I want.
My question is how can i get #select to correctly quote the column names?
Thank you
I don't think you can make the select method to protect your column names when using symbols (maybe because different DBMS use different quoting identifiers), but you could pass your selection as a string :
sql = Model.where(:foo => bar).select("[open], [high], [low], [close]").to_sql
Model.connection.execute(sql)
I attempted to submit a bug report to Rails, however in doing so I saw the problem did not appear to exists using SQLite test case, this leads me to believe the issue is with the SQL Server Adapter.
Since I am on Rails 4 and not the latest version of the adapter I left it and wrote the following (horrible) method as wrapping the column names was not enough, I needed to prefix the table to prevent ambiguous column names. Yuck
def self.quote(*columns, klass)
columns.map { |col| "[#{klass.table_name}].[#{col}]" }.join(', ')
end

Rails refuses to store a value in a column named `authorization`

I've fought a few hours now to store a string in a database column in Rails.
I had to rename authorization to transaction so that Rails would store the value.
Why does Rails interfere while saving the value?
Example:
# Works
self.update_attribute(:transaction, result) rescue nil
# Does not work
self.update_attribute(:authorization, result) rescue nil
What is your underlying database? It might have "authorization" as a reserved word.
See the generated sql and run it directly to your db. If it runs without problems, then my assumption is invalid.
Both mySQL and SQLserver use authorization as a reserved word.
So you'll just need to use the different word.
You could also use something close like 'authorized' or 'auth'.
maybe try prefixing the column using the table name? For example:
UPDATE my_table
SET my_table.authorization = "new authorization"
WHERE id = 5

rails 3 find command not working with postgresql database

I'm using ruby 1.9.2 and rails 3 with a postgresql 8.4 database.
I have the following piece of rails code.
#playersonline = Member.find(:all, :conditions => ["loggedIn = ?", true] )
And I get the following error when the line is encountered:
PGError: ERROR: column "loggedin" does not exist
Looking at the query it generates it shows the following:
SELECT "members".* FROM "members" WHERE (loggedIn = 't')
The loggedIn column does exist in my table, and it has a boolean data type.
Another thing that is odd, when I try to query just the loggedIn column via a sql browser I get the same error? i.e. select loggedIn from members
Thanks
Postgres is case-sensitive by default so loggedIn isn't the same as loggedin. Although you can override it, Rails convention is that variable name-parts are separates by an underscore.
I wouldn't recommend re-inventing the wheel. Go in and change the columnn name (and any other columns that follow your notation) you might have to logged_in. This will prevent you from encountering any more strange errors.

Resources