Why would you set `null: false, default: ""` on a required DB column? - mysql

I'm building a Rails app with Devise for authentication, and its default DB migration sets the following columns:
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
What is the purpose of setting null: false and default: "" at the same time?
My understanding is that null: false effectively makes a value required: i.e., trying to save a record with a NULL value in that column will fail on the database level, without depending any validations on the model.
But then default: "" basically undoes that by simply converting NULL values to empty strings before saving.
I understand that for an optional column, you want to reject NULL values just to make sure that all data within that column is of the same type. However, in this case, email and password are decidedly not optional attributes on a user authentication model. I'm sure there are validations in the model to make sure you can't create a user with an empty email address, but why would you set default: "" here in the first place? Does it serve some benefit or prevent some edge case that I haven't considered?

Broadly speaking:
To make a column required, you must set null: false on it. This is true whether you are creating a new table or updating an existing one.
And in the event that you're updating an existing table, the DB engine will try to populate that new column with NULL in each row. In such cases, you must override this behavior with default: "", or else it will conflict with null: false and the migraiton will fail.
With respect to Devise:
Devise uses two separate templates for building migrations: migration.rb, for creating new tables, and migration_existing.rb, for updating existing tables (see source on GitHub). Both templates call the same migration_data method to generate the lines in question (i.e., the ones that specify null: false, default: ""), but as mentioned above, default: "" is only really relevant in the latter case (see O. Jones’ answer for more).
So the short answer to your question, specifically in the case of Devise migrations, is “because the generator uses borrowed code which doesn’t always apply, but still doesn’t break anything.”
A consideration for UNIQUE columns:
Note that in most popular SQL engines, uniquely indexed columns can still contain multiple rows of NULL values, as long as they are not required (naturally). But the effect of making a new column both required and unique (i.e., null: false, default: "", and unique: true) is that it cannot be added: the DB engine tries to populate the new column with an empty string in each row, which conflicts with the unique constraint and causes the migration to fail.
(The only scenario in which this mechanism fails is if you have exactly one row in your table — it gets a blank string value for the new column, which naturally passes the uniqueness constraint because it's the only record.)
So another way to look at it might be that these options are a safety mechanism preventing you from running migrations that you shouldn't (i.e., retroactively adding required columns to an already-populated table).

There is a difference in the insertion type. For example, let say you have a new_table table such that:
CREATE TABLE IF NOT EXISTS `new_table` (
`col1` VARCHAR(10) NOT NULL,
`col2` VARCHAR(10) NOT NULL DEFAULT '',
`col3` VARCHAR(10) NULL DEFAULT '');
When you use explicit insert of NULL you'll get the NULL:
INSERT INTO new_table(col1,col2,col3) VALUES('a','b',NULL);
'a','b',NULL
for col2 same trick will result in error:
INSERT INTO new_table(col1,col2,col3) VALUES('a',NULL,'c');
But when you use implicit insert of NULL you'll get the default value:
INSERT INTO new_table(col1,col2) VALUES('a','b');
'a','b',''
meaning that setting a default value is not preventing NULL assertion to this column, but only used when the value is not explicitly given.

Some application software gacks on NULL values but not on zero-length text strings. In Oracle, they're the same thing, but not in MySQL.
Things get interesting upon altering tables to add columns. In that case a default value is mandatory, so the DBMS can populate the new column.

I'm thinking this is here because of MySQL 'strict mode' not allowing you to disallow a null value without providing a default.
From mysql docs: https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html
For data entry into a NOT NULL column that has no explicit DEFAULT clause, if an INSERT or REPLACE statement includes no value for the column, or an UPDATE statement sets the column to NULL, MySQL handles the column according to the SQL mode in effect at the time: If strict SQL mode is enabled, an error occurs for transactional tables and the statement is rolled back. For nontransactional tables, an error occurs, but if this happens for the second or subsequent row of a multiple-row statement, the preceding rows are inserted. If strict mode is not enabled, MySQL sets the column to the implicit default value for the column data type.

Related

Sequelize & mysql: clause `[tableName].deleted_at IS NULL` getting added automatically

using mysql + sequelize in a React/Node app;
a where the clause below gets automatically added which is screwing up my other where clause statements
[tableName].deleted_at IS NULL
in the nodeJS console, I can see the queries which all have this where clause...
tried recreating indexes, deleting this column throws an error that this field is not available;
column definition is:
`deleted_at` DATETIME NULL DEFAULT NULL,
Sequelize models provide you with a table settings option called paranoid.
In short, it's providing you with a deleted state for each row in that table, and that flag will be included in every query while using the ORM methods to work with the DB on this model.
So when you set the paranoid option to true, deletedAt column will be added as date and time and null as default.
MyModel.init({ /* attributes here */ }, {
sequelize,
paranoid: true,
// If you want to give a custom name to the deletedAt column
deletedAt: 'destroyTime'
});
If you actually check the raw queries, you will find the addition to your WHERE statement WHERE deletedAt IS NULL on all the queries.
You can find out more from the official Sequelize documentation here:
https://sequelize.org/master/manual/paranoid.html

Rails sees mysql tinyint(1) as a boolean - but I want it to be a number

Rails 4.2.1 using mysql2 gem. ActiveRecord treats a mysql column with data type tinyint(1) as a boolean. But I want to use it as a small number - I want to store values up to 100 which is ok for tinyint(1). When I try to create a record, the tinyint column casts to false and I get a depreciation warning:
> Foo.create(my_tinyint_col: 13)
(0.2ms) BEGIN
SQL (0.5ms) INSERT INTO `foos` (`my_tinyint_col`) VALUES (0)
(107.3ms) COMMIT
=> #<Foo ID: 519, my_tinyint_col: false>
DEPRECATION WARNING: You attempted to assign a value which is not
explicitly true or false to a boolean column. Currently this value
casts to false. This will change to match Ruby's semantics, and will
cast to true in Rails 5. If you would like to maintain the current
behavior, you should explicitly handle the values you would like cast
to false.
If i change the data definition of my_tinyint_col to tinyint(2) the problem goes away - but is there a way to treat tinyint(1) as a number using ActiveRecord?
Same issue occurs when tinyint(1) is seen as boolean by Hibernate. The trick is not to use tinyint(1) but to use tinyint(4). Doing so, RoR won't assume that it is boolean.
At any rate, there is actually no difference between tinyint(1) and tinyint(2) in MySQL. Both can hold the same values - 1 and 2 are signifiers of column width only.
See this please
For people who found this issue via google search: if you have the same problem but don't want to actually change the column data type you can add this line to your model.
attribute :my_tinyint_col, ActiveRecord::Type::Integer.new
Further reading: https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html

Rails 4.1 - Write to MySQL database without typecasting

I have a column in my MySQL database which is of type TINYINT(1). I need to store actual integers in this column. The problem is, because of the column type, Rails 4.1 assumes this column contains only boolean values, so it typecasts all values besides 0 or 1 to be 0 when it writes to the database.
I don't want to simply disable boolean emulation since we have a number of columns in our database where we use TINYINT(1) to actually represent a boolean value. And I am currently not able to change the column types in MySQL.
How can I force Rails 4.1 to bypass the typecasting step and write directly to the database instead?
(This excerpt from the Rails 4.1 source may be of some use: https://github.com/rails/rails/blob/4-1-stable/activerecord/lib/active_record/attribute_methods/write.rb)
Could you use raw SQL to do the insert?
Something like:
sql = "INSERT INTO my_table (smallnumber) VALUES (100)"
ActiveRecord::Base.connection.execute(sql)
I don't know if it works but you can try to overwrite the setter using the method :raw_write_attribute or :write_attribute. The :raw_write_attribute and :write_attribute methods disable/enable the type casting before writing.
Let's say the attribute/column is called :the_boolean_column_who_wanted_to_be_an_integer, you can probably do something like:
def the_boolean_column_who_wanted_to_be_an_integer=(value)
raw_write_attribute(:the_boolean_column_who_wanted_to_be_an_integer, value) # or write_attribute(...
end
Does it work?
Maybe you should overwrite the setter completely, using rails 4.1 source code:
def the_field=(value)
attr_name = 'the_field'
attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
#attributes_cache.delete(attr_name)
column = column_for_attribute(attr_name)
# If we're dealing with a binary column, write the data to the cache
# so we don't attempt to typecast multiple times.
if column && column.binary?
#attributes_cache[attr_name] = value
end
if column || #attributes.has_key?(attr_name)
#attributes[attr_name] = value
else
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
end
end
Note that #attributes[attr_name] = send(type_cast_method, column, value) has been changed to #attributes[attr_name] = value . You can probably simplify it for your use case. Also note that I haven't tried this, and even if it works, you should be careful whenever you want to upgrade rails.
Plan A: Change to SMALLINT (2 bytes) as a compromise.
Plan B: See if TINYINT(3) will fool Rails into not thinking it is Boolean.
Plan C: See if TINYINT UNSIGNED will fool Rails into not thinking it is Boolean. (This assumes your number are non-negative: 0..255.)

MS Access default value

I have a text column in one of my MS Access tables that is empty be default when a new record is inserted. The problem I have is that I can't seem to check whether this field is empty with EITHER equals Null or equals "" . For example, neither of these "reads" the field as being empty:
If [Field] = "" Or [Field] = Null Then
I've always used Access 2007 to develop this database, and I recently opened it with Access 2003, which is when I think this problem started. Could that be the cause? If so, would simply opening and saving it again with Access 2007 solve it?
When you compare against null, you need to use the IsNull function. With traditional ANSI SQL logic, Null <> Null, so you need a special function to test for null.
If [Field] = "" Or IsNull([Field])
First off, I would suggest that you do one of two things:
set your fields to disallow zero-length strings. That way you'd have to test only Is Null.
if you feel you must allow storage of ZLS, then set the default value to a ZLS. It's still possible for the field to end up Null, though, so this may or may not be helpful.
I don't see storage of ZLS's as having any utility whatsoever. It's almost always a shortcut to accomodate data being appended from sources that return ZLS's instead of Nulls for empty fields.
You should also read up on Nulls. Allen Browne has a number of pages that explain it all quite well:
Nulls: Do I need them?
Common Errors with Null
Aspects of working with Nulls in VBA code:
Nothing? Empty? Missing? Null?
The articles are Access-oriented, but could be valuable to those using any database, particularly relative novices because of the conversational style of the writing.
You should try:
If Nz([Field], "") = "" Then
Because:
If [Field] = ""
in:
If [Field] = "" Or IsNull([Field])
Would still throw a Null Error!

linq to sql string property from non-null column with default

I have a LINQ to SQL class "VoucherRecord" based on a simple table. One property "Note" is a string that represents an nvarchar(255) column, which is non-nullable and has a default value of empty string ('').
If I instantiate a VoucherRecord the initial value of the Note property is null. If I add it using a DataContext's InsertOnSubmit method, I get a SQL error message:
Cannot insert the value NULL into column 'Note', table 'foo.bar.tblVoucher'; column does not allow nulls. INSERT fails.
Why isn't the database default kicking in? What sort of query could bypass the default anyway? How do I view the generated sql for this action?
Thanks for your help!
If you omit the column, the value becomes the database default, but anything you insert is used instead of the default, example:
INSERT INTO MyTable (ID, VoucherRecord) Values(34, NULL) -- Null is used
INSERT INTO MyTable (ID) Values(34) -- Default is used
Picture for example you have a column that defaults to anything but NULL, but you specifically want NULL...for that to ever work, whatever value you specify MUST override the default, even in the case of NULL.
You need to set Auto-Sync to OnInsert, Auto Generated Value to true and Nullable to false for your column to work. See here for a full run-down with explanation on the Linq side.
For viewing the generated SQL, I have to recommend LinqPad