-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
RFC: Declarative definition of FOREIGN KEY and UNIQUE constraints #2712
Conversation
The spike is ready for review. |
|
||
Individual indexes can be defined as follows: | ||
|
||
- Add a new field `properties` as a key-value map from property names to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite follow the meaning of "Individual indexes", could you explain more about it?
I understand the example above uses LB3 syntax:
{
strict: false,
forceID: true,
indexes: {
uniqueEmail: {
// index definition
},
nameQueries: {
// index definition
}
}
}
While confused about the individual index...is it a syntax supported in LB3 or a proposal that will be supported?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have same comment as @jannyHou .
Do you mean
uniqueEmail: {
// index definition
},
can be filled in like this?:
uniqueEmail: {
properties: {
email: 1, // ASC
createdAt: 'DESC', // alias for -1
bio: 'text', // database-specific value (MongoDB's "text")
}
},
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for not being more clear. In model definition, indexes
property contains a key-value map from "index name" to "index definition". By "individual index", I mean an entry in this map for an index.
The example posted by @emonddr is correct. I am cross-posting the snippet from examples/todo-list
code that can be found at the bottom of this pull request.
@model({
indexes: {
/*==== Dummy demonstration of a model-level index definition ===*/
demo: {
properties: {desc: 'ASC'},
mysql: {
kind: 'FULLTEXT',
},
},
},
foreignKeys: {
/*==== Dummy demonstration of a model-level foreign-key definition ===*/
demo: {
sourceProperties: ['todoListId'],
targetModel: () => TodoList,
targetProperties: ['id'],
onDelete: 'CASCADE',
},
},
})
export class Todo extends Entity {
// ...
}
properties: { | ||
email: 'ASC', | ||
}, | ||
mongodb: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might misunderstand your proposal here...wouldn't the database configurations stay inside each model property's definition like:
properties: {
email: {
type: 'string',
index: true,
mongodb: {sparse: true},
mysql: {kind: 'fulltext', type: 'hash'}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am talking about database-specific configuration of an index. Please note the code snippet we are commenting on is showing definition of an index, not a model.
@model({
indexes: {
demo: {
properties: {email: 'ASC'},
mongodb: {
sparse: true
},
},
},
})
export class MyModel extends Entity {
@property()
email: string;
// ...
}
connector cannot process. The flag can allow three values: | ||
|
||
- true: abort migration with error | ||
- 'warn': print a warning and ignore unsupported metadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good, I would suggest warn
as default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on warning to be the default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please clarify our questions on individual indexes. thx.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I mostly come from the users' perspective, so might miss out on some of the technical details.
_SPIKE_.md
Outdated
|
||
- UNIQUE index with no special configuration | ||
|
||
```js |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case of Cloudant and CouchDB, they don't have UNIQUE index as you mentioned... If I'm using Cloudant as the datasource and happen to have unique
set. What's going to happen? We'll ignore it because it's not supported by the database?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the only unique field in cloudant/couchdb is the document id _id
, other loopback flavor constrains will be ignored.
See https://forums.couchbase.com/t/can-i-do-unique-constraint-on-some-attribute-not-the-id/5621
and https://stackoverflow.com/questions/1541239/unique-constraints-in-couchdb
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in "Additional changes" below, I am proposing to ignore such index (as we are doing now), but let the user know that the constraints expected by the model will not be enforced by the database.
When a model specifies an index or constraint that's not supported by the
connector running automigration, the connector should let the user know about
the problem. To preserve backwards compatibility, I am proposing to print a
console warning only.
_SPIKE_.md
Outdated
|
||
### Decorators | ||
|
||
I am proposing to reuse existing `@property` and `@model` decorators. They are |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on reusing existing decorators from the users' point of view.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about relationship decorators such as @belongsTo
, @hasMany
, @hasOne
. I know we recently added support to add property data to those decorators, but I don't think it is something we want to encourage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(EDITED) See my comment above. See my other comment: #2712 (comment)
The new property-definition field references
will be usable with the relational decorators supporting custom property definition metadata.
@model()
class Todo extends Entity {
// ...
@belongsTo(
() => TodoList,
{},
{
/*==== Define a foreign key to enforce referential integrity ===*/
references: {
model: () => TodoList,
property: 'id',
onDelete: 'CASCADE',
},
},
)
todoListId: number;
}
connector cannot process. The flag can allow three values: | ||
|
||
- true: abort migration with error | ||
- 'warn': print a warning and ignore unsupported metadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 on warning to be the default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple questions, but I like the changes.
_SPIKE_.md
Outdated
|
||
### Decorators | ||
|
||
I am proposing to reuse existing `@property` and `@model` decorators. They are |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about relationship decorators such as @belongsTo
, @hasMany
, @hasOne
. I know we recently added support to add property data to those decorators, but I don't think it is something we want to encourage.
|
||
In the contrary with the current implementation, I am proposing that `columns` | ||
should replace any column list created from `properties` and `keys`, and that | ||
`columns` should be nested inside connector-specific options: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with the syntax for postgresql.columns = ['lower (email) ASC']
. Just want to make sure what will happen if a user defines both, properties
and postgreqsl.columns
.
It seems like the connector will just use whatever is defined in the connector property. I don't know how you could know programmatically that [lower(email) ASC
was intended to replace email
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how you could know programmatically that
lower(email) ASC
was intended to replace
Exactly! That's why I am proposing that columns
should take precedence over keys
/properties
and replace anything defined via those key/value maps.
- If
postgresql.columns
is defined, then the connector usescolumns
and ignoredkeys
&properties
. - If
postgresql.columns
is not defined, the connector will merge entries defined bykeys
andproperties
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If postgresql.columns is not defined, the connector will merge entries defined by keys and properties.
So the order is postgresql.columns
-> keys
-> properties
, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
If columns
are set, then keys
and properties
are ignored completely.
Otherwise the content from keys
and properties
is merged, e.g. the following definition will create a composite index over two columns.
{
keys: {
name: 'ASC',
},
properties: {
email: 'DESC',
}
}
|
||
### Foreign keys at property level | ||
|
||
Introduce a new property metadata "references" (inspired by ANSI SQL): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of references, but isn't this duplicating data? How can this example be applied to the TodoList example? In the todo model, we define todoListId as belongsTo
(aka references) TodoList
Maybe there is a way to leverage or combine this syntax with relational decorators?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of references, but isn't this duplicating data? How can this example be applied to the TodoList example?
I have modified the TodoList example to show how the new syntax could be used, see the bottom half of this pull request.
I feel we are not duplicating data here, because belongsTo
decorator can be used without referential integrity, just to navigate to related models in GraphQL style.
Also as we have discussed in the past, we need to rethink how relation decorators are applied. At the moment, @hasMany
is applied on the navigational property (todoList.todos
) while @belongsTo
is applied on the foreign key (todo.todoListId
) - this is inconsistent & possibly confusing. The navigational property does not belong to the model base class because relational data is not part of model data, create/update operations are not able to update related data in a single call. So far, we are leaning to rework decorators to be applied on the model constructor, and let the foreign-key property be defined independently of the relation.
Having said that, I can imagine that for now, we can modify @belongsTo
decorator to add references
metadata to the property definition it creates. The difficult part is how to infer keyTo
(the primary key of the target model) at the time the decorator is invoked.
if (!('references' in propertyDefinition)) {
propertyDefinition.references = {
model: targetResolver,
property: keyTo,
};
}
const propMeta: PropertyDefinition = Object.assign(
// etc. (no change)
);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you all for valuable feedback!
_SPIKE_.md
Outdated
|
||
- UNIQUE index with no special configuration | ||
|
||
```js |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in "Additional changes" below, I am proposing to ignore such index (as we are doing now), but let the user know that the constraints expected by the model will not be enforced by the database.
When a model specifies an index or constraint that's not supported by the
connector running automigration, the connector should let the user know about
the problem. To preserve backwards compatibility, I am proposing to print a
console warning only.
|
||
Individual indexes can be defined as follows: | ||
|
||
- Add a new field `properties` as a key-value map from property names to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for not being more clear. In model definition, indexes
property contains a key-value map from "index name" to "index definition". By "individual index", I mean an entry in this map for an index.
The example posted by @emonddr is correct. I am cross-posting the snippet from examples/todo-list
code that can be found at the bottom of this pull request.
@model({
indexes: {
/*==== Dummy demonstration of a model-level index definition ===*/
demo: {
properties: {desc: 'ASC'},
mysql: {
kind: 'FULLTEXT',
},
},
},
foreignKeys: {
/*==== Dummy demonstration of a model-level foreign-key definition ===*/
demo: {
sourceProperties: ['todoListId'],
targetModel: () => TodoList,
targetProperties: ['id'],
onDelete: 'CASCADE',
},
},
})
export class Todo extends Entity {
// ...
}
properties: { | ||
email: 'ASC', | ||
}, | ||
mongodb: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am talking about database-specific configuration of an index. Please note the code snippet we are commenting on is showing definition of an index, not a model.
@model({
indexes: {
demo: {
properties: {email: 'ASC'},
mongodb: {
sparse: true
},
},
},
})
export class MyModel extends Entity {
@property()
email: string;
// ...
}
|
||
In the contrary with the current implementation, I am proposing that `columns` | ||
should replace any column list created from `properties` and `keys`, and that | ||
`columns` should be nested inside connector-specific options: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how you could know programmatically that
lower(email) ASC
was intended to replace
Exactly! That's why I am proposing that columns
should take precedence over keys
/properties
and replace anything defined via those key/value maps.
- If
postgresql.columns
is defined, then the connector usescolumns
and ignoredkeys
&properties
. - If
postgresql.columns
is not defined, the connector will merge entries defined bykeys
andproperties
.
|
||
### Foreign keys at property level | ||
|
||
Introduce a new property metadata "references" (inspired by ANSI SQL): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of references, but isn't this duplicating data? How can this example be applied to the TodoList example?
I have modified the TodoList example to show how the new syntax could be used, see the bottom half of this pull request.
I feel we are not duplicating data here, because belongsTo
decorator can be used without referential integrity, just to navigate to related models in GraphQL style.
Also as we have discussed in the past, we need to rethink how relation decorators are applied. At the moment, @hasMany
is applied on the navigational property (todoList.todos
) while @belongsTo
is applied on the foreign key (todo.todoListId
) - this is inconsistent & possibly confusing. The navigational property does not belong to the model base class because relational data is not part of model data, create/update operations are not able to update related data in a single call. So far, we are leaning to rework decorators to be applied on the model constructor, and let the foreign-key property be defined independently of the relation.
Having said that, I can imagine that for now, we can modify @belongsTo
decorator to add references
metadata to the property definition it creates. The difficult part is how to infer keyTo
(the primary key of the target model) at the time the decorator is invoked.
if (!('references' in propertyDefinition)) {
propertyDefinition.references = {
model: targetResolver,
property: keyTo,
};
}
const propMeta: PropertyDefinition = Object.assign(
// etc. (no change)
);
_SPIKE_.md
Outdated
|
||
### Decorators | ||
|
||
I am proposing to reuse existing `@property` and `@model` decorators. They are |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(EDITED) See my comment above. See my other comment: #2712 (comment)
The new property-definition field references
will be usable with the relational decorators supporting custom property definition metadata.
@model()
class Todo extends Entity {
// ...
@belongsTo(
() => TodoList,
{},
{
/*==== Define a foreign key to enforce referential integrity ===*/
references: {
model: () => TodoList,
property: 'id',
onDelete: 'CASCADE',
},
},
)
todoListId: number;
}
80d4c0d
to
f4a4539
Compare
```ts | ||
@property({ | ||
type: 'string', | ||
unique: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we can just support index: {unique: true}
to avoid the unique
keyword. Do we need to support unique
constraint beyond indexing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the syntax unique: true
shorter and easier to write than index: {unique: true}
.
Also the longer form index: {unique: true}
is opening doors for adding additional index metadata, e.g. index: {unique: true, sparse: true}
. I am little bit concerned that it makes it easier for users to define something that's not supported - see the other comment below.
@raymondfeng What are your concerns, why would you like to avoid adding the new keyword unique
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the shorthand is more convenient - at least for first iteration.
```ts | ||
@property({ | ||
type: 'string', | ||
index: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we allow index: boolean | IndexOptions
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my previous answer. My intention is to keep property-level indexes simple and ask users to define a model-level index if they need anything more complex.
Allowed all IndexOptions
properties is opening doors to complex combinations, for example how do you propose to treat property-level index defined as follows?
class MyModel extends Entity {
@property({
index: {
properties: {
// "name" is a different property
name: 'ASC',
}
},
postgresql: {
// custom "columns" typically replace
// configuration provided by properties/keys
columns: ['lower(email) ASC'],
}
})
email: string;
@property()
name: string;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the detailed overview of the current status of FK and unique constraints. I've read through the proposal and the spike code demonstrating the interfaces and decorators to be created. Overall, I agree with the direction of the spike and the next steps.
required: true, | ||
references: { | ||
// a TypeResolver | ||
model: () => Category, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering why we'd like to support three ways of declaring the target model here. Can we keep this simple and support the typeResolver flavour only (at least for first iteration)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point 👍
|
||
In the contrary with the current implementation, I am proposing that `columns` | ||
should replace any column list created from `properties` and `keys`, and that | ||
`columns` should be nested inside connector-specific options: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If postgresql.columns is not defined, the connector will merge entries defined by keys and properties.
So the order is postgresql.columns
-> keys
-> properties
, correct?
- Supported by: MySQL, PostgreSQL | ||
- Missing support in: MSSQL, Oracle | ||
- Not possible to implement in: MongoDB, CouchDB, Cloudant | ||
- Not supported: `ON UPDATE` and `ON DELETE` options, e.g. `ON DELETE CASCADE` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add tasks for supporting these features in the connectors as part of next actions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My expectation is that we will create those tasks as part of "Spike: a template implementation of index & constraint migration in SqlConnector
". I'll update capture this information in the spike acceptance criteria.
4029988
to
7f92406
Compare
FWIW, we already have a community-contributed pull request adding support for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have limited knowledge on the implementation details, but your proposal looks good to me as an overview. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👏
Created the following follow-up stories:
Closing this pull request as done. |
See #2606
To support strong relations with referential integrity enforced by the database, we need a mechanism allowing models to define FOREIGN KEY and UNIQUE constraints in a declarative way.
Please review my proposal outlined in
_SPIKE_.md
, you can also review the changes proposed in model/property definition interfaces (TypeScript types).Checklist
👉 Read and sign the CLA (Contributor License Agreement) 👈
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated👉 Check out how to submit a PR 👈