How do you add a Unique case insensitive Index to a PostgresSQL table using EF Fluent API - postgresql

I have recently been using PostgreSQL rather than SQL, so finding a lot of little nuances between the two.
I want to be able to make a string value unique in a table, so using the EF code first fluent API, I have this code.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MyTable>()
.HasIndex(u => u.UniqueValue)
.IsUnique();
base.OnModelCreating(modelBuilder);
}
When creating a migration, it will generate this.
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_MyTable_UniqueValue",
table: "MyTable",
column: "UniqueValue",
unique: true);
}
This will then add the index to the PostgreSQL table, and work when the word is of the same case.
e.g. Try and insert "Hello" twice, and it will not work.
It does allow for variations of the word though, so I can insert "Hello", "HEllo", "HELlo", etc...
It looks like it is possible to force the case on the index in PostgreSQL using something like
CREATE UNIQUE INDEX UniqueValue ON MyTable (UPPER(UniqueValue));
However I am not sure how to do this via EF Fluent API and create the migration from that?

It seems like for now you'll have to set up a raw SQL migration. Support for that still hasn't been added (yet). You could also set up a generated (computed) column that holds the result of upper(UniqueValue), then add a unique index on that.
There's no way to add an expression-based unique constraint or to add the expression to an existing unique index key. You can build a new index using your definition as is:
CREATE UNIQUE INDEX UniqueValue ON MyTable (UPPER(UniqueValue));
Or add an exclusion constraint which ends up doing pretty much the same (as of now, quite far from being supported in EF):
create table test(txt text);
alter table test
add constraint case_insensitive_unique
exclude using gist(upper(txt) with =);
insert into test(txt) select 'hello';
--INSERT 0 1
insert into test(txt) select 'Hello';
--ERROR: conflicting key value violates exclusion constraint "case_insensitive_unique"
--DETAIL: Key (upper(txt))=(HELLO) conflicts with existing key (upper(txt))=(HELLO).
Demo

Related

How to run a subquery when inserting into a table in EFCore

I have an orderid integer column in a Postgres table called Orders and it's not the primary key. And there's a logic to automatically increment it considering the max value of the orderid and adding 1 to it.
One solution to this is creating a function in the database layer and set it as the default value to this column. But since that couples us to the database layer, we are thinking of something that's related to EF. We can't pass a SQL as the default value to orderid because postgres gives an error. We can also make it an auto-increment value but we don't wanna do that because the logic could change.
What we want is a way to run this subquery and automatically generate the value to orderid.
I presume what you want to achieve here is to maintain a separate non-primary key, auto-incremented unique id which is number. You can this by using a sequence.
1) Create the sequence via fluent API
2) Set the column to use the next value from the sequence
3) Make sure to create and run migrations before testing.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasSequence<int>("OrderNumbers")
.StartsAt(1000)
.IncrementsBy(1);
modelBuilder.Entity<Order>()
.Property(o => o.OrderNo)
.HasDefaultValueSql("nextval('\"OrderNumbers\"')");
}

How to change entry values for DbContext.SaveChanges()

We are trying to implement a multi tenant architecture in our Web API based application. We are using RLS in SQL Server, and Subscription_Id is, what is given to each subscriber. We have set the default value for Subscription_Id in SQL Server, so while I am calling db.SaveChanges(), I just want to ignore the Subscription_Id going to the SQL Server from the API.
I tried setting the value of Subscription_Id in the SaveChanges() override method but got stuck here.
public override int SaveChanges()
{
var objectType = selectedEntity.CurrentValues.ToObject();
Guid value = new Guid("54E720FC-616B-44C6-8485-5F2185FD7B4C");
PropertyInfo propertyInfo =
objectType.GetType().GetProperty("Subscription_Id");
ChangeTracker.Entries().FirstOrDefault()
.CurrentValues.ToObject().GetType()
.GetProperty("Subscription_Id")
.SetValue(objectType, Convert.ChangeType(value, propertyInfo.PropertyType), null);
return base.SaveChanges();
}
My advice is that you shouldn't modify your SaveChanges() code for this.
A recommended way of using RLS is making the TenantId columns transparent to your EF model and your code, so you don't need to define Tenant ID or navigation properties in your entities. This way you don't need to change your SaveChanges() code, or to explicitly manage and set Subscription_Id values anywhere in your code other that when opening the DB connection.
What you need to do is manually setting a default value constraint in the Subscription_Id columns in your database, with a default value based on the current session Subscription_Id parameter. The value will be set when inserting the records, and implicitly used to filter any subsequent queries and commands at database level.
In case of a new column:
ALTER TABLE SomeEntityTable ADD Subscription_Id nvarchar(128)
DEFAULT CAST(SESSION_CONTEXT(N'UserId') AS nvarchar(128))
In case of an existing column:
ALTER TABLE SomeEntityTable
ADD DEFAULT CAST(SESSION_CONTEXT(N'UserId') AS nvarchar(128)
FOR Subscription_Id
If the column had a previous different DEFAULT value it would be good to also delete its associated obsolete DEFAULT constraint. More info about updating default values in existing columns can be found here.
These columns should not be included in your model. You should not have properties for them in your entity classes. If you are using Database First you should make sure you exclude/ignore these columns when updating your model from your database.
How to do this if you are using EF Code First: you can manually include AlterColumn (or CreateColumn) instructions in a code migration after you generate it with Add-Migration. Do it for every entity table:
public override void Up()
{
AlterColumn("dbo.SomeEntityTable", "Subscription_Id",
c => c.String(
nullable: false,
maxLength: 128,
defaultValueSql: "CAST(SESSION_CONTEXT(N'UserId') AS nvarchar(128))"));
}
(It would be good to add also a Down() method removing the column.)
Warning: Be careful when running this migration if you already have existing records in the tables with an empty Subscription_Id column value (or if you are adding a new Subscription_Id column to a table that already have records). The empty column will be filled with the value of the Subscription_Id in the connection that is executing the migration, which probably will be wrong (you probably don't want all the existing records to be associated to that specific subscription). In that case you may want to include explicit UPDATE instructions with the right Subscription_Id values in your Up() method, with the Sql() method. Something like this:
Sql("UPDATE SomeEntitiesTable SET Subscription_Id= '19bc9b0d-28dd-4510-bd5e-d6b6d445f511' WHERE Id IN (1, 2, 5)");
With Code First you should also remove the Subscription_Id properties from your model classes. If you can't, at least add explicit Ignore() instructions in your configuration code for the Subscription_Id columns, you don't want them in your EF mappings.
Note: I'm assuming here that you created a RLS policy in your DB that uses UserId parameter in SESSION_CONTEXT, and that your application code is setting that value when opening the DB connection, via a DbConnectionInterceptor or something similar.
This page contains more info.

Fuelphp: convert standard ORM model to model_temporal

Using fuelphp, is there a way to modify an existing \ORM\Model to convert it to \ORM\Model_Temporal? I started looking into creating a migration, which needs to 1) add the necessary columns to my table (not difficult); 2) update the primary key to include the new columns as part of a compound primary key (difficult). The problem I am having is that I don't see a method in the DBUtil that performs this operation.
What is the right way to do this?
Currently DBUtil does not include a way to alter primary keys because the method of doing so differs between DB systems and DBUtil is not actually that aware what DBMS you are using.
To do this you will have to construct the query manually in your migration and use DB::query() to execute it.
In case anyone else is faced with the same task, here is what I ended up doing. My database is MySQL, the syntax may have to be changed if you're using a different DBMS. My original primary key was an auto-incrementing column named id.
class Modify_TableName_To_Temporal
{
public function up()
{
\DBUtil::add_fields('tablename',array(
'temporal_start'=>array('constraint'=>11, 'type'=>'int','default'=>0),
'temporal_end'=>array('constraint'=>11, 'type'=>'int','default'=>2147483647),
));
\DB::query('ALTER TABLE tablename MODIFY id int, DROP PRIMARY KEY')->execute();
\DB::query('ALTER TABLE tablename ADD PRIMARY KEY (id, temporal_start, temporal_end), MODIFY id int auto_increment')->execute();
}
public function down()
{
\DB::query('ALTER TABLE tablename MODIFY id int, DROP CONSTRAINT primary')->execute();
\DB::query('ALTER TABLE tablename ADD PRIMARY KEY (id), MODIFY id int auto_increment')->execute();
\DBUtil::drop_fields('tablename',array('temporal_start','temporal_end'));
}
}

Entity Framework can't migrate MaxLength value when column has index

I try to change MaxLength property from 100 to 50 and i got exception that Says
"The index 'IX_Singers_Name' is dependent on column 'Name'.
ALTER TABLE ALTER COLUMN Name failed because one or more objects access this column."
Mode is :
public class Singer : NamedEntity
{
[MaxLength(50)] // It was 100
public override string Name { get; set; }
}
As i understand, entity framework needs to alter table for this change but it can't alter table because an index exist on Name property. So how i can make it possible with entity framework migrations ?
I can possibly drop index in migration then change maxlength in next migration and create index the last migration again. But i believe that there should be exist an easy way to change that attribute value.
In SQL Server, indexes are pretty much like tables themselves. So if you've got the column in an index, both that index and the table would need to be modified. I agree that where EF migrations were scaffolded to add the index (e.g. for a foreign key) they should also take care of removing and reapplying the index. However, in this instance the index would have had to have been added manually. Therefore it will need to be maintained manually in the migration. Note that it can be done in a single migration:
public override void Up()
{
DropIndex("dbo.Singer", new []{"Name"});
/* Code to alter the table */
CreateIndex("dbo.Singer", "Name");
}
Don't forget to put this in both the Up() and Down() methods.

Entity Framework Code First ignoring specific schema

Using Entity Framework 4.3.1 CodeFirst and having no luck getting the migrations or scripts to respect the schema that I want my tables to end up in.
It seems the default behavior (the one that I'm seeing regardless of what I do) is to omit the schema completely from the SQL that actually runs causes tables to be created in the default schema for the user running the migration or script.
My DBAs are telling me that they cannot change my default schema due to the fact that I'm part of an AD group and not a local user, so changing the default schema for the user running (an often recommended workaround) the script is not an option at all.
I've tried using the annotations like this:
[Table("MyTable", Schema = "dbo")]
public class MyTable
{
public int Id { get; set; }
public string MyProp1 { get; set; }
public string MyProp2 { get; set; }
}
And I've also tried using the fluent variant of the same thing:
modelBuilder.Entity<YourType>().ToTable("MyTable", "dbo");
The resultant script (and migrations) ignore the fact that I tried to specify a schema. The script looks like this:
CREATE TABLE [MyTable] (
[Id] [int] NOT NULL IDENTITY,
[MyProp1] [nvarchar](max),
[MyProp2] [nvarchar](max),
CONSTRAINT [PK_MyTable] PRIMARY KEY ([Id])
)
When there should be a [dbo] tucked in there like this:
CREATE TABLE [dbo].[MyTable] (
[Id] [int] NOT NULL IDENTITY,
[MyProp1] [nvarchar](max),
[MyProp2] [nvarchar](max),
CONSTRAINT [PK_MyTable] PRIMARY KEY ([Id])
)
Has anyone else had luck in getting Entity Framework to respect the schema? This behavior pretty much kills our ability to use codefirst at all in our enterprise environment.
Reminder: Changing my user to have a different default schema is not an option at all.
As my comment seems to have answered the quandary, I am recreating it as an answer.
It seems that because the SQL Server provider uses "dbo" as the default schema, it will not explicitly add "dbo" to the TSQL that creates the tables even if you specify that in your configuration.
This answers the basic problem. But now I am curious if dbo is the default, do you (Bob) still have a reason to specify it? It doesn't hurt to have it in the configuration if you just want the default to be obvious to someone reading the code. But is the behavior creating another side-effect?
ADDED: Aha! FIXED IN EF5! :) (I just updated my test project to use EF5RC (against .NET 4.0) and I got "dbo" explicitly in the TSQL for create table.)
I tried all of the stuff that Bob Bland tried with a similar lack of success (I was also using Entity Framework 4.3.1 CodeFirst). Then I changed the generated migration to look like this and it worked. Maybe this will save somebody a few minutes of pain?
So my solution was to generate the migration as normal, then hack it by hand to include dbo. as shown below.
public override void Up()
{
CreateTable(
"dbo.UploadedFiles", // See? I have added dbo. to the front of my table name :-)
c => new
{
UploadedFileId = c.Guid(nullable: false, identity: true),
// other columns omitted for brevity...
FileData = c.Binary(),
})
.PrimaryKey(t => t.UploadedFileId);
}
and the Down bit looks like this
public override void Down()
{
DropTable("dbo.UploadedFiles"); // See? I have added dbo. to the front of my table name here too :-)
}