ballerina-platform / ballerina-library

The Ballerina Library
https://ballerina.io/learn/api-docs/ballerina/
Apache License 2.0
137 stars 65 forks source link

Add support for making entity field(s) as unique key and composite unique keys for SQL DB #5944

Closed daneshk closed 4 months ago

daneshk commented 9 months ago

Summary

The current bal persist design doesn't support making entity field(s) unique keys and composite unique keys. So there is no way to mark entity fields as unique keys. We need to support this for the DB introspection.

daneshk commented 9 months ago

The unique indexes and unique constraints are two different elements to consider when we use relational databases.

If you want better performance for queries then using a Unique Index would be best since it can help reduce processing time by providing quicker lookups in specified fields; however, if your main priority is data integrity then opting for a Unique Constraint would be best since it prevents any potential data inconsistency issues by making sure only valid entries can be added.

daneshk commented 9 months ago

Check different other implementations,

Prisma doesn't differentiate unique indexes vs unique constraints. AFAIU, they decides whether to create indexes or constraints based on the DB https://www.prisma.io/docs/orm/reference/prisma-schema-reference#unique

https://www.baeldung.com/jpa-unique-constraints https://www.baeldung.com/jpa-indexes

https://learn.microsoft.com/en-us/ef/core/modeling/indexes?tabs=data-annotations

https://gorm.io/docs/indexes.html

daneshk commented 9 months ago

Design consideration

Proposed Design

Based on the above, the initial proposed solution as follows, In the persist.sql model, we introduce new annotations called Index and Unique Index like below,

# Marks the entity field as an index field. 
public type IndexConfig record {|
    # The name used to define the index. This is required to group the fields for the composite index
    # If the field is mapped to two or more composite index groups, index names are listed in an array.
    string|string[]? name = ();
|};

# The Annotation indicates the index field in the `bal persist` model type.
 public annotation IndexConfig Index on record field;

# The Annotation indicates the unique index field in the `bal persist` model type.
 public annotation IndexConfig UniqueIndex on record field;

So developer can give the mapping DB table name and column names for the record and record field using Map annotation in the data model definition as follows,

type Doctor record {| readonly int id; @sql:Index string name; string specialty; @sql:UniqueIndex string phoneNumber; Appointment[] appointment; |};


* For composite index and unique index fields
```ballerina
import ballerina/persist as _;
import ballerinax/persist.sql;

#  As the index names of both `name` and `specialty` and the same, they form a composite index.
type Doctor record {|
    readonly int id;
    @sql:Index("idx_id")
    string name;
    @sql:Index("idx_id")
    string specialty;
    string phoneNumber;
    Appointment[] appointment;
|};

Here the name field is included in two different composite indexes.

Ballerina doesn't allow the addition of the same annotation multiple times to the same entity fields. Hence index names are recorded as string array values.

type Doctor record {| readonly int id; @sql:Index(["idx_specialty", "idx_name"]) string name; @sql:Index("idx_specialty") string specialty; @sql:Index("idx_name") string phoneNumber; Appointment[] appointment; |};

sameerajayasoma commented 7 months ago

IMO, the ideal solution is this.

#  Here the name field is included in two different composite indexes.
type Doctor record {|
    readonly int id;
    @sql:Index {name: "idx_specialty" }
    @sql:Index {name: "idx_name" }
    string name;
    @sql:Index {name: "idx_specialty"}
    string specialty;
    @sql:Index {name: "idx_name"}
    string phoneNumber;
    Appointment[] appointment;
|};

But, you cannot declare the same annotation twice for a single attachment point. On the other hand, a table can have one or more indexes and each index has a name. With that definition, I believe the following should work. I prefer name instead of names.

#  Here the name field is included in two different composite indexes.
type Doctor record {|
    readonly int id;
    @sql:Index {name: ["idx_name", "idx_specialty"] }
    string name;
    @sql:Index {name: "idx_specialty"}
    string specialty;
    @sql:Index {name: "idx_name"}
    string phoneNumber;
    Appointment[] appointment;
|};
daneshk commented 4 months ago

This feature is implemented. Hence closing the issue