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

DPLT-1136: Implement update, upsert, and delete on context.db #189

Merged
merged 4 commits into from Sep 7, 2023

Conversation

darunrs
Copy link
Collaborator

@darunrs darunrs commented Aug 31, 2023

NOTE: PR targeting my active PR #188 for now to show accurate diff. Once that PR is merged, I'll switch this PR back to main (Or open a new one) and do a push to fix the commit history and reopen for review. This is just to be available to look at in the mean time.

PR 188 has been merged and I have rebased and force pushed the history to this PR. There are no conflicts with main.

Adding functionality for update, upsert, delete. Added relevant tests as well as performed simple integration tests using existing tests. No problems found there. Will do a full test in dev after push as well by updating a forked social_feed indexer to use context.db exclusively.

Copy link
Contributor

@roshaans roshaans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job, lgtm overall. Just left some comments on testing and conventions.

}

async upsert (schemaName: string, tableName: string, objects: any[], conflictColumns: string[], updateColumns: string[]): Promise<any[]> {
await this.initialized; // Ensure constructor completed before proceeding
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems odd that we are awaiting a variable here. How about calling await this.initialize() and then inside of the initialize() method, you can change it to return early if it is already initialized.

You can also change the function signature of initialize() to return a Boolean rather than a void to make it more clearer.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I went with a variable is because I want the initialize function to be called in the constructor. However, I cannot await a function there. So, instead, I call it while setting the result to a variable. That way, I can call the function and not await in the constructor. Then, when a method is called, it just checks to see if the variable has been set. This was the most common pattern I could find for calling an async method in the constructor. It gives an error if I try to call the function as is. The return type of Promise is because we don't actually care about the result of the function, just that it finished and set the variable to something.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found another way to work around async constructors:

Instead of creating the class via the constructor itself, we can create static method to do it. This method could be async, and enforce all the additional constraints that weren't possible in a normal constructor. We can then make the contructor private so that you have to use the new method, e.g.

export default class DmlHandler {
  private constructor (
    private readonly account: string,
    private readonly hasuraClient: HasuraClient,
    private readonly pgClient: PgClient,
  ) {}

  static async new (
    account: string,
    hasuraClient: HasuraClient = new HasuraClient(),
    PgClient = PgClientModule,
  ): Promise<DmlHandler> {
    const connectionParameters = await hasuraClient.getDbConnectionParameters(account);
    const pgClient = new PgClient({
      user: connectionParameters.username,
      password: connectionParameters.password,
      host: process.env.PGHOST,
      port: Number(connectionParameters.port),
      database: connectionParameters.database,
    });

    return new DmlHanlder(account, hasuraClient, pgClient);
  }
}

We no longer need the await this.initialize since we are passing in an already initialised client. We would create the class it like so:

const dmlHandler = await DmlHandler.new('morgs.near');

Now that construction of DmlHandler is async, we would have to change every class which uses it to also follow this pattern. This probably isn't something to commit to right now, but should be something we work towards as it provides a lot more flexibility.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's neat! I'll give it a shot. Also updated the PR name.

@@ -507,38 +507,106 @@ CREATE TABLE
expect(resultLimit.length).toEqual(1);
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it also makes sense to cover the non-happy path scenarios such as testing to see if the update, delete, upsert methods fail as expected when garbage, malformed, or nonexistent table or field values are passed in to them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we actually do not support failure when invalid input is provided as we wouldn't know that unless we do one of two things:

  1. Run the query itself.
  2. Know what the input object structure should look like.

1 is not helpful and 2, is a work in progress. We need to do the type generation on the schema to get table object types. We can use that to ensure good information is passed in. Unfortunately for now, we don't have that. The best I could do was use typescript strong typing to ensure the inputs sort of look normal.

@morgsmccauley
Copy link
Collaborator

Can you please add the jira ticket to the PR title i.e. DPLT-XXXX feat: Implement...

runner/src/dml-handler/dml-handler.test.ts Outdated Show resolved Hide resolved
}

async upsert (schemaName: string, tableName: string, objects: any[], conflictColumns: string[], updateColumns: string[]): Promise<any[]> {
await this.initialized; // Ensure constructor completed before proceeding
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found another way to work around async constructors:

Instead of creating the class via the constructor itself, we can create static method to do it. This method could be async, and enforce all the additional constraints that weren't possible in a normal constructor. We can then make the contructor private so that you have to use the new method, e.g.

export default class DmlHandler {
  private constructor (
    private readonly account: string,
    private readonly hasuraClient: HasuraClient,
    private readonly pgClient: PgClient,
  ) {}

  static async new (
    account: string,
    hasuraClient: HasuraClient = new HasuraClient(),
    PgClient = PgClientModule,
  ): Promise<DmlHandler> {
    const connectionParameters = await hasuraClient.getDbConnectionParameters(account);
    const pgClient = new PgClient({
      user: connectionParameters.username,
      password: connectionParameters.password,
      host: process.env.PGHOST,
      port: Number(connectionParameters.port),
      database: connectionParameters.database,
    });

    return new DmlHanlder(account, hasuraClient, pgClient);
  }
}

We no longer need the await this.initialize since we are passing in an already initialised client. We would create the class it like so:

const dmlHandler = await DmlHandler.new('morgs.near');

Now that construction of DmlHandler is async, we would have to change every class which uses it to also follow this pattern. This probably isn't something to commit to right now, but should be something we work towards as it provides a lot more flexibility.

@darunrs darunrs changed the title feat: Implement update, upsert, and delete on context.db DPLT-1136: Implement update, upsert, and delete on context.db Sep 5, 2023
@@ -258,6 +258,7 @@ export default class Indexer {
try {
const tables = this.getTableNames(schema);
let dmlHandler: DmlHandler | null = null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can this be null?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under the impression that I could not use ?? on an uninitialized variable. I tested that in some online compilers and got an error. However, I just removed it now and it seems to be working. So, I will make it let dmlHandler: DmlHandler;.

Base automatically changed from fixTableRegex to main September 6, 2023 17:54
Copy link
Collaborator

@morgsmccauley morgsmccauley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great job

@darunrs darunrs merged commit f0c3a53 into main Sep 7, 2023
3 checks passed
@darunrs darunrs deleted the finishDbMethods branch September 7, 2023 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants