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

Feat: Conditional insert #578

Open
tsanton opened this issue Apr 29, 2024 · 3 comments
Open

Feat: Conditional insert #578

tsanton opened this issue Apr 29, 2024 · 3 comments
Assignees

Comments

@tsanton
Copy link

tsanton commented Apr 29, 2024

One of the, in my option, larger limitations with EF at the moment is the lack for conditional singleton inserts.

My current case is as follows: I'm allowing users to manipulate a history table, but with certain limitations.
For instance I will allow them to create a new statuses, but that status can't be backdated with "valid_from" <= min(valid_from) where entity was created (status == 'created').

As of now I have to look up the entity (or run an .Any() with a predicate), and then insert if it passes the predicate, whereas I'd much rather just fire off IQueryable.Where(prediates).ConditionalInsertAsync(Entity) and return the count from the output to see if one went in or if 0 inserted (and then return conditional responses based on the feedback).

In terms of design (at least for Postgres) I'm thinking something along these lines:

CREATE TEMP TABLE proof_of_concept (
    id UUID PRIMARY KEY,
    name TEXT,
    age INTEGER,
    created TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

--Empty table
select * from proof_of_concept;

--Initial insert
INSERT INTO proof_of_concept
select gen_random_uuid() id, 'Al Coholic' name, 10 age, timezone('utc', now()) created;

--Al made his way into the model
select * from proof_of_concept;

--Do not insert because the new age (9) is lesser than his his current max age.
with data as(
    select 'Al Coholic' name, 9 age
)

INSERT INTO proof_of_concept
select gen_random_uuid() id, data.name, data.age, timezone('utc', now()) created from data
where data.age > (select max(poc.age) from proof_of_concept poc where name = data.name)
RETURNING *;

--As we can see, both from RETURNING and select: no insert
select * from proof_of_concept;

--Do insert because the new age (11) is greater than his his current max age (10)
with data as(
    select 'Al Coholic' name, 11 age
)

INSERT INTO proof_of_concept
select gen_random_uuid() id, data.name, data.age, timezone('utc', now()) created from data
where data.age > (select max(poc.age) from proof_of_concept poc where name = data.name)
RETURNING *;

--And here we have it: conditional insert
select * from proof_of_concept;

I'm posting the suggestion here firstly because I think a lot of the required pipework for this extension already exist within the existing code base. Further I think it's a killer extension that I'm somewhat perplex that I can't find an implementation for -> it surely would save a lot of time and boilerplate code.

I can also say that though it's on the EF core radar (here) I would not put money on it making the EF core 9 cut. Nor is it completely clear to me if the design supports the conditional bit.

Hoping to hear back from you and I'd be happy to help with other SQL-provider syntax research or whatever you feel you might need in order to get this into either extension or plus!

/T

@JonathanMagnan JonathanMagnan self-assigned this Apr 29, 2024
@JonathanMagnan
Copy link
Member

JonathanMagnan commented Apr 29, 2024

Hello @tsanton ,

Here are a few questions to better understand your requirement

  1. Are you using PostgreSQL?
  2. Are you looking to provide a list of entities to insert or looking to insert from existing entities (already in the database)? Such as what InsertFromQuery does?
  3. Is an option like InsertStagingTableFilterFormula that will allow you to write your own SQL (for the filtering part in the WHERE statement) and filter data before they get inserted enough for you?
    • NOTE: This option works for SQL Server but not yet for PostgreSQL

Let me know more about your scenario. Depending on your answer, we might already be very close to supporting it.

Best Regards,

Jon

@tsanton
Copy link
Author

tsanton commented Apr 29, 2024

Hi @JonathanMagnan!

I'm on Postgres, yes.
I would like to utilise this in place of EF.AddAsync (so not from query, but single parameterised insert from code). I am agnostic to how this is solved on the backend, with a CTE/temp-table with insert from query, or parameterized select :)

I'm already running this pattern for updates and deletes. Here is how I implement a conditional delete of "SomeEntity" based on the ValidFrom queryable predicate

var query = from ua in context.SomeRandomEntity
    let minValidFrom = (
        from x in context.SomeRandomEntity
        where x.TenantId == tenantId && x.SomeId == pred.SomeId
        select x.ValidFrom
    ).Min()
    where ua.TenantId == tenantId && ua.SomeId == pred.SomeId && ua.Id == pred.Id && ua.ValidFrom > minValidFrom
    select ua;

var strategy = context.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
    await using var transaction = await context.Database.BeginTransactionAsync(ct);
    var deleted = await query.ExecuteDeleteAsync(ct);
    if (deleted != 1)
    {
        await transaction.RollbackAsync(ct);
        return false;
    }
    await transaction.CommitAsync(ct);
    return true;
});

@JonathanMagnan
Copy link
Member

Thank you for the additional information.

We will work on it.

Best Regards,

Jon

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

No branches or pull requests

2 participants