Skip to content

Conversation

@p5quared
Copy link
Contributor

@p5quared p5quared commented Oct 14, 2024

Description of changes

Update TS schema generator to properly annotate serial fields from Postgres datasources.

e.g.:
myField SERIAL -> myField: a.integer().default()

Explanation:
We already populate field.default when importing a DB schema, but don't really do anything with it prior to this change.

Now, we check if the field default is DB_GENERATED, and the value for the generated field indicates it is a SERIAL column. If this is the case, we annotate the TS schema field with .default().

CDK / CloudFormation Parameters Changed

N/A

Issue #, if available

N/A

Description of how you validated changes

See test cases in

  • packages/amplify-graphql-schema-generator/src/__tests__/generate-ts-data-schema.test.ts
  • packages/amplify-graphql-schema-generator/src/__tests__/pg-string-datasource-adapter.test.ts

Checklist

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@p5quared p5quared force-pushed the feat/serial-schema-generation branch 2 times, most recently from 8d4f420 to f411e70 Compare October 14, 2024 22:48
"
`;

exports[`generate graphqlSchemaFromSQLSchema creates graphql schema from "postgres" "serial" schema 1`] = `
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This snapshot is not intentional as we are not intending to support GQL generation here. It is generated automatically to test against anyway from the schemas in __utils__.

expect(graphqlSchema).toMatchSnapshot();
});

it.each([
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These cases are not exhaustive over every Field that could exist but cover the most unique cases.

@p5quared p5quared force-pushed the feat/serial-schema-generation branch from f411e70 to 09e08e8 Compare October 14, 2024 23:03
@p5quared p5quared force-pushed the feat/serial-schema-generation branch from 09e08e8 to b793733 Compare October 14, 2024 23:03
const createDataType = (type: FieldType): ts.Node => {
if (type.kind === 'Scalar') {
const createDataType = (field: Field): ts.Node => {
const sequenceRegex = /^nextval\(.+::regclass\)$/;
Copy link
Contributor Author

@p5quared p5quared Oct 14, 2024

Choose a reason for hiding this comment

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

Regex Explain

I went back and forth on a few approaches to this check but I eventually felt that a quick regex is the simplest surefire way to identify SERIAL fields. In the future I think this logic could be extracted into something like if (canLetDBGenerate(field)) which evaluates rules isSerial(field) || isUserDefinedFunction(field) etc... But I felt that might be premature for this implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For more context, here's what a value for column_default looks like on a serial field when we query the DB schema:
nextval('serial_table_number_seq'::regclass).

Copy link
Member

Choose a reason for hiding this comment

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

Can we find documentation that this is the canonical way to determine whether a field is serial? In the past we've relied on experimentally-verified "rules" that turned out to be incomplete, so I'd love to get ahead of that this time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added main reply with my research/argument later in thread. I don't believe there is a truly canonical way to identify serial but we can identify if a column is backed by a sequence.

@p5quared p5quared marked this pull request as ready for review October 14, 2024 23:20
@p5quared p5quared requested review from a team as code owners October 14, 2024 23:20
@p5quared p5quared changed the title Feat/serial schema generation feat: generate ts schema from pg serial field Oct 14, 2024
Comment on lines +262 to +265
\\"CoffeeQueue\\": a.model({
id: a.integer().default(),
name: a.string(),
orderNumber: a.integer().default()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the desired behavior of this PR.

]
`;

exports[`testPostgresStringDataSourceAdapter sets the model field correctly from a schema with a SERIAL field 1`] = `
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This behavior is not new but I am codifying it since generation depends on it now.

}
type SerialTable @refersTo(name: \\" serial_table\\") @model {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we know why we have blank spaces before the table name for refersTo directive?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whitespace in my template string with the field info csv was causing this--resolved.


it('sets the model field correctly from a schema with a SERIAL field', () => {
const adapter = new PostgresStringDataSourceAdapter(schemas.postgres.serial);
expect(adapter.getModels()[0].getFields()).toMatchSnapshot();
Copy link
Contributor

Choose a reason for hiding this comment

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

Before this validation, can we add a check to make sure getModels contains an element? If there are failures, the current implementation will throw a generic error like trying to access a property of undefined which could be hard to troubleshoot.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I modified this to match the other tests, e.g. just adapter.getModels(). I think it's clearer and it also retains info that about the PK:

 "primaryKey": Index {
      "fields": Array [
        "id",
      ],
      "name": "PRIMARY_KEY",
    },

if (type.kind === 'Scalar') {
const createDataType = (field: Field): ts.Node => {
const sequenceRegex = /^nextval\(.+::regclass\)$/;
if (field.default?.kind === 'DB_GENERATED' && sequenceRegex.test(field.default.value.toString())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can possible move the check sequenceRegex.test(field.default.value.toString() to a separate method so that it can reused in the future like isSequenceField(field).

Comment on lines +97 to +98
const sequenceRegex = /^nextval\(.+::regclass\)$/;
return field.default?.kind === 'DB_GENERATED' && sequenceRegex.test(field.default.value.toString());
Copy link
Member

Choose a reason for hiding this comment

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

(Copying this from main conversation comment)

Can we find documentation that this is the canonical way to determine whether a field is serial? In the past we've relied on experimentally-verified "rules" that turned out to be incomplete, so I'd love to get ahead of that this time.

Copy link
Contributor Author

@p5quared p5quared Oct 21, 2024

Choose a reason for hiding this comment

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

In summary we cannot determine if a column was declared serial, we can identify sequences though, and from this, infer serial fields.

Ultimately serial doesn't really exist as a datatype we can directly introspect. In PG it is basically a macro for "Create a column as an integer and create a sequence to back that column as a default". We end up with an ordinary non-null integer with a special default value.

We can tell if a column is backed by a sequence since its default will be nextval(_), one of the special sequence functions.

Reference.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the thorough explanation!

@palpatim palpatim merged commit 6b87331 into main Oct 22, 2024
7 checks passed
@palpatim palpatim deleted the feat/serial-schema-generation branch October 22, 2024 14:55
tejas2008 pushed a commit that referenced this pull request Oct 29, 2024
* chore: add missing types

* test: ts schema serial field gen happy case

* test: ts schema serial field gen sad cases

* chore: add DEFAULT_METHOD to TS schema constants

* feat: intepret pg serial fields in ts schema gen

* test: do not handle db provided int constants

* refactor: simpler serial field default condition

* test: pg string adapter sets model field correctly from a schema with a SERIAL field

* test: align text in template string

the text alignment was causing weird names in snaphot

* test: serial gen snapshots against all of getModels()

* refactor: extract sequence field identification logic

* chore: no-op change to force rebuild
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.

3 participants