Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SQL Ledger support #33226

Open
Tealons opened this issue Mar 3, 2024 · 15 comments
Open

Add SQL Ledger support #33226

Tealons opened this issue Mar 3, 2024 · 15 comments

Comments

@Tealons
Copy link

Tealons commented Mar 3, 2024

In our project we want to add the Ledger feature to several tables that have high integrity requirements. We are planning to use the built-in Ledger support of MSSQL: https://learn.microsoft.com/en-us/sql/relational-databases/security/ledger/ledger-overview?view=sql-server-ver16

The problem is that because this feature is missing from EF Core, we need to create our own custom queries for migrations and exclude these tables from the EF Core migrations.

Could you please consider adding support for the Ledger feature which will enabled to create normal EF Core migrations for our tables.

@ErikEJ
Copy link
Contributor

ErikEJ commented Mar 3, 2024

Would database model first work for you?

@Tealons
Copy link
Author

Tealons commented Mar 3, 2024

It's not what I was hoping for, but I will take anything to have a more integrated situation :)

@roji
Copy link
Member

roji commented Mar 11, 2024

Note the similarity with temporal tables (docs).

In any case, at least the main part of this would purely be a migration feature (from the docs: "Ledger and the historical data are managed transparently, offering protection without any application changes.").

As with most such advanced migration features, it should be possible to use raw SQL in migrations to get the desired table definition (either by adding ALTER TABLE statements, or by replacing the CreateTable operation by a CREAT TABLE). While this isn't as nice as having EF model everything, it's unfeasible for EF to cover the DDL for every possible database feature.

@Tealons
Copy link
Author

Tealons commented Mar 11, 2024

In this case the migration is what I'm looking for. I tried it with a raw SQL, but combining it with the versioning did not work (right away).

@roji
Copy link
Member

roji commented Mar 11, 2024

combining it with the versioning did not work

Which versioning are you referring to? Note that EF wouldn't be able to do anything that you're not capable of doing yourself in raw SQL - that's why there's also relatively little value in adding support for such features in migrations.

@Tealons
Copy link
Author

Tealons commented Mar 11, 2024

I added .ToTable("Cars", b => b.IsTemporal());. This generates a lot in the migrations. I tried to combine this with the ledger, but failed at it. But then again, I did not try very hard yet. Maybe removing IsTemporal is the solution, but I don't know if that will remove the temporal Linq query capabilities?

@roji
Copy link
Member

roji commented Mar 11, 2024

@Tealons from these docs, it sounds like it should be possible for the same table to be both temporal and ledger-backed... But that's something you'll need to figure out - I suggest looking for help in SQL Server forums (at this point it's no longer an EF-related question).

@roji
Copy link
Member

roji commented Mar 11, 2024

Putting this in the backlog to gather user feedback.

@roji roji added this to the Backlog milestone Mar 11, 2024
@Tealons
Copy link
Author

Tealons commented Mar 17, 2024

@roji: Indeed this is possible. I was just looking for a way via EF Core to create migrations for both temporal and ledger enabled tables. Now I use the b => b.IsTemporal() method were EF creates al the necessary migrations and makes temporal queries possible. I can also do this via raw sql, but I'm not sure how this would work in combination with the temporal option I now have enabled. But I will do some research next week and when I have something that works, I will also post it here.

@the-programmer
Copy link

the-programmer commented Apr 4, 2024

As suggested in here #33226 (comment). For me a "code first" model would be preferred.
However since I need to implement this for a project, instructions for creating a database first model are also welcome.

@ErikEJ
Copy link
Contributor

ErikEJ commented Apr 4, 2024

@the-programmer Is anything special needed for the database first scenario? Have you tried reverse engineering a ledger table?

@the-programmer
Copy link

The only thing special is that it has to be combined with a code first model. So far I haven't tried anything, but reverse engineering is one of the topics I have to look into.
Also I would prefer a way to add the ledger table in a migration. However a manual SQL command in there is fine for me.

@the-programmer
Copy link

the-programmer commented Apr 4, 2024

Ok, So I added the table manually

CREATE TABLE [Measurements]
   (
	  Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
	  [ImportantFloat] FLOAT NOT NULL,
	  [ImportantNumber] BIGINT NOT NULL, 
          [Timestamp] DatetimeOffset NOT NULL
   )
   WITH (LEDGER = ON (APPEND_ONLY = ON));

And the reverse engineering also worked (using EF Core Power Tools).
With this I now have the following
Model

public class Measurement
{
    public int Id { get; set; }
    public double ImportantFloat { get; set; }
    public long ImportantNumber { get; set; }
    public DateTimeOffset Timestamp { get; set; }
    public long LedgerStartTransactionId { get; set; }
    public long LedgerStartSequenceNumber { get; set; }
}

DbContext

public virtual DbSet<Measurement> Measurements { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //....
    modelBuilder.Entity<Measurement>(entity =>
    {
        entity.HasKey(e => e.Id).HasName("PK____3214EC075AA505CF");
        entity.Property(e => e.LedgerStartSequenceNumber).HasColumnName("ledger_start_sequence_number");
        entity.Property(e => e.LedgerStartTransactionId).HasColumnName("ledger_start_transaction_id");
    });
}

(Yes, it has some manual edits)

However if I now do a "add-migration" the Measurements table gets added to my migration chain. I'm not 100% sure what to do with this migration. Can I just update it so the resulting migration to something like the following or what is best?

migrationBuilder.Sql(@"CREATE TABLE [Measurements]
   (
	  Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
	  [ImportantFloat] FLOAT NOT NULL,
	  [ImportantNumber] BIGINT NOT NULL, 
          [Timestamp] DatetimeOffset NOT NULL
   )
   WITH (LEDGER = ON (APPEND_ONLY = ON));");

@roji
Copy link
Member

roji commented Apr 4, 2024

@the-programmer you haven't shown the actual CreateTable migration created when doing add-migration. But yeah, you can simply replace that with the CREATE TABLE SQL above. A safe way would be to generate the SQL migration script from the original migration (which EF created), and then simply copy that SQL back into the migration as the replacement, adding the WITH (LEDGER... ).

@the-programmer
Copy link

the-programmer commented Apr 5, 2024

@roji, I didn't think the CreateTable was required since that was "automatic".

But anyway, hor reference for someone else who might need this in the future (until ledger support is officially there).

  • Add the table manually via SQL.
  • Use reverse engineering to grab the full model from the database. (I used the option to create a 2nd context file)
  • (if required) move the model "OnModelCreating" code to your dbContext. (Ensure the PRIMARY KEY is set correctly, otherwise you might have to update the ModelSnapshot).
  • Run add-migration.
  • Edit the migration so it looks like below (note, as you can see I updated the PRIMARY KEY part since we have a constant name this way.)
  • Delete the ledger table from the database.
  • Run update-database so the code in the migration runs.
  • Done? (if not, let me know)
    public partial class AddLedger: Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.Sql(@"CREATE TABLE [Measurements]
                                   (
	                              [Id] INT NOT NULL IDENTITY,
	                              [ImportantFloat ] FLOAT NOT NULL,
	                              [ImportantNumber] BIGINT NOT NULL, 
                                      [Timestamp] DatetimeOffset NOT NULL,
	                              CONSTRAINT [PK_Measurements] PRIMARY KEY ([Id]),
                                   )
                                   WITH (LEDGER = ON (APPEND_ONLY = ON));");
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Measurements");
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants