This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-ent-adapter.git


The following commit(s) were added to refs/heads/master by this push:
     new ec45d2b  feat: enforce the unique index on policy rules
ec45d2b is described below

commit ec45d2bec696b3b0d1a8c5111c78a55adec463da
Author: Yang Luo <[email protected]>
AuthorDate: Thu Aug 6 22:15:34 2026 +0800

    feat: enforce the unique index on policy rules
---
 README.md                    |  49 ++++
 adapter.go                   | 206 ++++++++++++----
 adapter_test.go              |  69 ++++++
 ent/casbinrule/casbinrule.go |  14 ++
 ent/casbinrule_create.go     | 546 +++++++++++++++++++++++++++++++++++++++++++
 ent/casbinrule_update.go     |  86 +++++++
 ent/generate.go              |   4 +-
 ent/migrate/schema.go        |  21 +-
 ent/runtime.go               |  14 ++
 ent/schema/casbinrule.go     |  37 ++-
 go.sum                       |  10 +
 11 files changed, 994 insertions(+), 62 deletions(-)

diff --git a/README.md b/README.md
index 90eb448..a2de5dd 100644
--- a/README.md
+++ b/README.md
@@ -125,6 +125,55 @@ func main() {
 
 The database used in the adapter should be created manually before calling 
`NewAdapter`. The adapter will automatically create the `casbin_rule` table if 
it doesn't exist.
 
+## Upgrading an existing database
+
+A policy rule is now unique on `(ptype, v0, v1, v2, v3, v4, v5)`, and the seven
+columns are `varchar(100)` so that the index fits within the 3072-byte key 
limit
+InnoDB enforces. Both are enforced by the automatic migration that runs inside
+`NewAdapter` and `NewAdapterWithClient`, so a database written by an earlier
+version needs a look before the first start on this version. A fresh database
+needs nothing.
+
+Check for rules that no longer fit. The adapter runs this check itself and
+refuses to migrate while it returns anything, because a MySQL server running
+without strict mode would truncate the values rather than reject them:
+
+```sql
+SELECT * FROM casbin_rules
+WHERE CHAR_LENGTH(ptype) > 100 OR CHAR_LENGTH(v0) > 100 OR CHAR_LENGTH(v1) > 
100
+   OR CHAR_LENGTH(v2) > 100 OR CHAR_LENGTH(v3) > 100 OR CHAR_LENGTH(v4) > 100
+   OR CHAR_LENGTH(v5) > 100;
+```
+
+Check for rules stored more than once. Nothing enforced uniqueness before, so
+duplicates may have accumulated, and the index cannot be built while they 
exist:
+
+```sql
+SELECT ptype, v0, v1, v2, v3, v4, v5, COUNT(*) AS copies
+FROM casbin_rules GROUP BY ptype, v0, v1, v2, v3, v4, v5 HAVING COUNT(*) > 1;
+```
+
+Duplicates are redundant by definition — casbin evaluates a rule the same way
+whether it is stored once or ten times — so keeping the lowest `id` of each
+group is safe:
+
+```sql
+DELETE c FROM casbin_rules c
+JOIN (
+  SELECT MIN(id) AS keep_id, ptype, v0, v1, v2, v3, v4, v5
+  FROM casbin_rules GROUP BY ptype, v0, v1, v2, v3, v4, v5 HAVING COUNT(*) > 1
+) d ON c.ptype = d.ptype AND c.v0 = d.v0 AND c.v1 = d.v1 AND c.v2 = d.v2
+   AND c.v3 = d.v3 AND c.v4 = d.v4 AND c.v5 = d.v5
+WHERE c.id > d.keep_id;
+```
+
+On PostgreSQL, use `DELETE FROM casbin_rules WHERE id NOT IN (SELECT MIN(id)
+FROM casbin_rules GROUP BY ptype, v0, v1, v2, v3, v4, v5);` instead.
+
+Adding a rule that is already stored stays a successful no-op rather than
+becoming an error, so replicas whose in-memory model has fallen behind another
+writer keep working.
+
 ## Getting Help
 
 - [Casbin](https://github.com/apache/casbin)
diff --git a/adapter.go b/adapter.go
index 2d29291..054724a 100644
--- a/adapter.go
+++ b/adapter.go
@@ -29,6 +29,7 @@ import (
 
        "github.com/casbin/casbin/v3/model"
        "github.com/apache/casbin-ent-adapter/ent"
+       "github.com/apache/casbin-ent-adapter/ent/schema"
        _ "github.com/go-sql-driver/mysql"
        _ "github.com/jackc/pgx/v5/stdlib"
        _ "github.com/lib/pq"
@@ -40,6 +41,21 @@ const (
        DefaultDatabase  = "casbin"
 )
 
+// batchSize caps how many rows go into a single INSERT.
+const batchSize = 5000
+
+// policyColumns are the columns covered by the unique index on a policy rule.
+// They double as the conflict target of every insert.
+var policyColumns = []string{
+       casbinrule.FieldPtype,
+       casbinrule.FieldV0,
+       casbinrule.FieldV1,
+       casbinrule.FieldV2,
+       casbinrule.FieldV3,
+       casbinrule.FieldV4,
+       casbinrule.FieldV5,
+}
+
 type Adapter struct {
        client *ent.Client
        ctx    context.Context
@@ -88,7 +104,7 @@ func NewAdapter(driverName, dataSourceName string, options 
...Option) (*Adapter,
                        return nil, err
                }
        }
-       if err := client.Schema.Create(a.ctx); err != nil {
+       if err := a.migrate(); err != nil {
                return nil, err
        }
        return a, nil
@@ -106,12 +122,69 @@ func NewAdapterWithClient(client *ent.Client, options 
...Option) (*Adapter, erro
                        return nil, err
                }
        }
-       if err := client.Schema.Create(a.ctx); err != nil {
+       if err := a.migrate(); err != nil {
                return nil, err
        }
        return a, nil
 }
 
+// migrate brings the schema up to date, guarding the two ways that can go 
wrong
+// on a database created before the policy tuple became unique.
+func (a *Adapter) migrate() error {
+       if err := a.checkFieldLengths(); err != nil {
+               return err
+       }
+       if err := a.client.Schema.Create(a.ctx); err != nil {
+               return explainDuplicateRules(err)
+       }
+       return nil
+}
+
+// checkFieldLengths refuses to migrate a table still holding policy values
+// longer than the column width that the unique index requires. Narrowing such 
a
+// column fails outright on a MySQL server in strict mode, but a server without
+// it truncates the values instead, quietly rewriting the stored policies.
+//
+// The probe is best effort. On a fresh database the table does not exist yet 
and
+// the query fails, which says nothing about anyone's data, so probe errors are
+// dropped and the migration itself is left to report real problems.
+func (a *Adapter) checkFieldLengths() error {
+       tooLong, err := a.client.CasbinRule.Query().Where(func(s 
*entsql.Selector) {
+               // LENGTH counts bytes on MySQL, so the portable spelling there 
is
+               // CHAR_LENGTH, which SQLite in turn does not have.
+               length := "CHAR_LENGTH"
+               if s.Dialect() == dialect.SQLite {
+                       length = "LENGTH"
+               }
+               overflows := make([]*entsql.Predicate, 0, len(policyColumns))
+               for _, column := range policyColumns {
+                       overflows = append(overflows, entsql.ExprP(
+                               fmt.Sprintf("%s(%s) > %d", length, s.C(column), 
schema.MaxFieldLen)))
+               }
+               s.Where(entsql.Or(overflows...))
+       }).Exist(a.ctx)
+       if err != nil || !tooLong {
+               return nil
+       }
+       return fmt.Errorf("casbin_rules holds policy values longer than %d 
characters, "+
+               "which the columns can no longer store; shorten them before 
upgrading, "+
+               "otherwise the migration truncates them", schema.MaxFieldLen)
+}
+
+// explainDuplicateRules names the policy data behind a migration that failed
+// because the table already holds rows the new unique index forbids. Drivers
+// report it as a bare constraint violation naming only an index.
+func explainDuplicateRules(err error) error {
+       msg := strings.ToLower(err.Error())
+       for _, marker := range []string{"duplicate entry", "duplicate key 
value", "unique constraint failed"} {
+               if strings.Contains(msg, marker) {
+                       return errors.Wrap(err, "casbin_rules holds duplicate 
policy rules, which the unique "+
+                               "index on (ptype, v0..v5) forbids; delete the 
redundant rows and retry")
+               }
+       }
+       return err
+}
+
 // LoadPolicy loads all policy rules from the storage.
 func (a *Adapter) LoadPolicy(model model.Model) error {
        policies, err := 
a.client.CasbinRule.Query().Order(ent.Asc("id")).All(a.ctx)
@@ -184,36 +257,15 @@ func (a *Adapter) SavePolicy(model model.Model) error {
                if _, err := tx.CasbinRule.Delete().Exec(a.ctx); err != nil {
                        return err
                }
-               lines := make([]*ent.CasbinRuleCreate, 0)
-
-               for ptype, ast := range model["p"] {
-                       for _, policy := range ast.Policy {
-                               line := a.savePolicyLine(tx, ptype, policy)
-                               lines = append(lines, line)
-                       }
-               }
+               lines := make([]policyLine, 0)
 
-               for ptype, ast := range model["g"] {
-                       for _, policy := range ast.Policy {
-                               line := a.savePolicyLine(tx, ptype, policy)
-                               lines = append(lines, line)
+               for _, sec := range []string{"p", "g"} {
+                       for ptype, ast := range model[sec] {
+                               lines = append(lines, policyLines(ptype, 
ast.Policy)...)
                        }
                }
 
-               // batch process
-               batchSize := 5000
-               for i := 0; i < len(lines); i += batchSize {
-                       end := i + batchSize
-                       if end > len(lines) {
-                               end = len(lines)
-                       }
-                       batch := lines[i:end]
-
-                       if _, err := 
tx.CasbinRule.CreateBulk(batch...).Save(a.ctx); err != nil {
-                               return err
-                       }
-               }
-               return nil
+               return a.insertPolicyLines(tx, lines)
        })
 }
 
@@ -221,8 +273,7 @@ func (a *Adapter) SavePolicy(model model.Model) error {
 // This is part of the Auto-Save feature.
 func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error {
        return a.WithTx(func(tx *ent.Tx) error {
-               _, err := a.savePolicyLine(tx, ptype, rule).Save(a.ctx)
-               return err
+               return a.insertPolicyLines(tx, []policyLine{{ptype: ptype, 
rule: rule}})
        })
 }
 
@@ -402,6 +453,83 @@ func (a *Adapter) savePolicyLine(tx *ent.Tx, ptype string, 
rule []string) *ent.C
        return line
 }
 
+// policyKey is the storage identity of a rule: the tuple covered by the unique
+// index. Two rules sharing a key map to the same row.
+type policyKey [7]string
+
+func newPolicyKey(ptype string, rule []string) policyKey {
+       key := policyKey{ptype}
+       for i := 0; i < len(rule) && i+1 < len(key); i++ {
+               key[i+1] = rule[i]
+       }
+       return key
+}
+
+// policyLine pairs a rule with its ptype so rules coming from different model
+// sections can be deduplicated against each other.
+type policyLine struct {
+       ptype string
+       rule  []string
+}
+
+func policyLines(ptype string, rules [][]string) []policyLine {
+       lines := make([]policyLine, 0, len(rules))
+       for _, rule := range rules {
+               lines = append(lines, policyLine{ptype: ptype, rule: rule})
+       }
+       return lines
+}
+
+// dedupPolicyLines keeps the first line of every distinct key. A casbin model
+// can legitimately carry duplicates -- loading a policy file appends each line
+// without checking whether the model already holds it -- and writing them out
+// unchanged would now collide with the unique index.
+func dedupPolicyLines(lines []policyLine) []policyLine {
+       seen := make(map[policyKey]struct{}, len(lines))
+       out := make([]policyLine, 0, len(lines))
+       for _, line := range lines {
+               key := newPolicyKey(line.ptype, line.rule)
+               if _, ok := seen[key]; ok {
+                       continue
+               }
+               seen[key] = struct{}{}
+               out = append(out, line)
+       }
+       return out
+}
+
+// insertPolicyLines writes lines in batches, leaving rows that already exist
+// untouched.
+//
+// Ignoring conflicts is what keeps writes idempotent now that the tuple is
+// unique. The enforcer screens duplicates against its in-memory model, but 
that
+// model is per-process: a second replica, or one whose model has gone stale,
+// can still submit a rule that is already stored. That used to insert a second
+// identical row; it must not start failing the caller instead.
+func (a *Adapter) insertPolicyLines(tx *ent.Tx, lines []policyLine) error {
+       lines = dedupPolicyLines(lines)
+       for start := 0; start < len(lines); start += batchSize {
+               end := start + batchSize
+               if end > len(lines) {
+                       end = len(lines)
+               }
+               batch := make([]*ent.CasbinRuleCreate, 0, end-start)
+               for _, line := range lines[start:end] {
+                       batch = append(batch, a.savePolicyLine(tx, line.ptype, 
line.rule))
+               }
+               // The conflict target is named explicitly because PostgreSQL 
requires
+               // an inference specification for DO UPDATE. MySQL has no 
target in its
+               // ON DUPLICATE KEY syntax and ignores it.
+               if err := tx.CasbinRule.CreateBulk(batch...).
+                       OnConflictColumns(policyColumns...).
+                       Ignore().
+                       Exec(a.ctx); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
 // UpdatePolicy updates a policy rule from storage.
 // This is part of the Auto-Save feature.
 func (a *Adapter) UpdatePolicy(sec string, ptype string, oldRule, newPolicy 
[]string) error {
@@ -445,14 +573,7 @@ func (a *Adapter) UpdatePolicies(sec string, ptype string, 
oldRules, newRules []
                                return err
                        }
                }
-               lines := make([]*ent.CasbinRuleCreate, 0)
-               for _, policy := range newRules {
-                       lines = append(lines, a.savePolicyLine(tx, ptype, 
policy))
-               }
-               if _, err := tx.CasbinRule.CreateBulk(lines...).Save(a.ctx); 
err != nil {
-                       return err
-               }
-               return nil
+               return a.insertPolicyLines(tx, policyLines(ptype, newRules))
        })
 }
 
@@ -513,14 +634,7 @@ func (a *Adapter) UpdateFilteredPolicies(sec string, ptype 
string, newPolicies [
 }
 
 func (a *Adapter) createPolicies(tx *ent.Tx, ptype string, policies 
[][]string) error {
-       lines := make([]*ent.CasbinRuleCreate, 0)
-       for _, policy := range policies {
-               lines = append(lines, a.savePolicyLine(tx, ptype, policy))
-       }
-       if _, err := tx.CasbinRule.CreateBulk(lines...).Save(a.ctx); err != nil 
{
-               return err
-       }
-       return nil
+       return a.insertPolicyLines(tx, policyLines(ptype, policies))
 }
 
 func CasbinRuleToStringArray(rule *ent.CasbinRule) []string {
diff --git a/adapter_test.go b/adapter_test.go
index 0a5fe67..21d58d5 100644
--- a/adapter_test.go
+++ b/adapter_test.go
@@ -23,6 +23,7 @@ import (
        "github.com/stretchr/testify/assert"
 
        "github.com/apache/casbin-ent-adapter/ent"
+       "github.com/apache/casbin-ent-adapter/ent/casbinrule"
 )
 
 func testGetPolicy(t *testing.T, e *casbin.Enforcer, res [][]string) {
@@ -263,6 +264,68 @@ func testFilteredPolicy(t *testing.T, a *Adapter) {
        testGetPolicy(t, e, [][]string{{"alice", "data1", "read"}, {"bob", 
"data2", "write"}})
 }
 
+// testUniqueIndex covers the index on (ptype, v0..v5): storing a rule that is
+// already there is a no-op rather than a second row or an error.
+func testUniqueIndex(t *testing.T, a *Adapter) {
+       // initAdapter has just stored the four rules of rbac_policy.csv.
+       count, err := a.client.CasbinRule.Query().Count(a.ctx)
+       assert.Nil(t, err)
+       assert.Equal(t, 4, count)
+
+       // Re-adding a stored rule is what a replica does when its in-memory 
model
+       // has not caught up with another writer.
+       assert.Nil(t, a.AddPolicy("p", "p", []string{"alice", "data1", "read"}))
+
+       // The same, in bulk, and with a duplicate inside the batch itself.
+       assert.Nil(t, a.AddPolicies("p", "p", [][]string{
+               {"alice", "data1", "read"},
+               {"eve", "data3", "read"},
+               {"eve", "data3", "read"},
+       }))
+
+       count, err = a.client.CasbinRule.Query().Count(a.ctx)
+       assert.Nil(t, err)
+       assert.Equal(t, 5, count)
+
+       eve, err := 
a.client.CasbinRule.Query().Where(casbinrule.V0EQ("eve")).Count(a.ctx)
+       assert.Nil(t, err)
+       assert.Equal(t, 1, eve)
+
+       // A model carrying the same rule twice must still save.
+       e, err := casbin.NewEnforcer("examples/rbac_model.conf")
+       assert.Nil(t, err)
+       m := e.GetModel()
+       assert.Nil(t, m.AddPolicy("p", "p", []string{"carol", "data4", "read"}))
+       assert.Nil(t, m.AddPolicy("p", "p", []string{"carol", "data4", "read"}))
+       assert.Nil(t, a.SavePolicy(m))
+
+       count, err = a.client.CasbinRule.Query().Count(a.ctx)
+       assert.Nil(t, err)
+       assert.Equal(t, 1, count)
+}
+
+func TestDedupPolicyLines(t *testing.T) {
+       lines := []policyLine{
+               {ptype: "p", rule: []string{"alice", "data1", "read"}},
+               {ptype: "p", rule: []string{"alice", "data1", "read"}},
+               {ptype: "p", rule: []string{"alice", "data1", "write"}},
+               {ptype: "g", rule: []string{"alice", "data1", "read"}},
+               // Trailing empty values are what the unused columns already 
default
+               // to, so this is the same row as the shorter rule above.
+               {ptype: "p", rule: []string{"alice", "data1", "write", ""}},
+       }
+
+       assert.Equal(t, []policyLine{lines[0], lines[2], lines[3]}, 
dedupPolicyLines(lines))
+}
+
+func TestNewPolicyKeyIgnoresExtraValues(t *testing.T) {
+       // Only Ptype and V0..V5 are stored, so a longer rule cannot overflow 
the
+       // key, and the values past V5 do not take part in its identity.
+       assert.Equal(t,
+               newPolicyKey("p", []string{"0", "1", "2", "3", "4", "5"}),
+               newPolicyKey("p", []string{"0", "1", "2", "3", "4", "5", "6"}))
+}
+
 func TestAdapters(t *testing.T) {
        a := initAdapter(t, "mysql", "root:@tcp(127.0.0.1:3306)/casbin")
        testAutoSave(t, a)
@@ -299,4 +362,10 @@ func TestAdapters(t *testing.T) {
        testUpdatePolicies(t, a)
        testUpdateFilteredPolicies(t, a)
        testFilteredPolicy(t, a)
+
+       a = initAdapter(t, "mysql", "root:@tcp(127.0.0.1:3306)/casbin")
+       testUniqueIndex(t, a)
+
+       a = initAdapter(t, "postgres", "user=postgres password=postgres 
host=127.0.0.1 port=5432 sslmode=disable dbname=casbin")
+       testUniqueIndex(t, a)
 }
diff --git a/ent/casbinrule/casbinrule.go b/ent/casbinrule/casbinrule.go
index d739603..a86f5cc 100644
--- a/ent/casbinrule/casbinrule.go
+++ b/ent/casbinrule/casbinrule.go
@@ -54,18 +54,32 @@ func ValidColumn(column string) bool {
 var (
        // DefaultPtype holds the default value on creation for the "Ptype" 
field.
        DefaultPtype string
+       // PtypeValidator is a validator for the "Ptype" field. It is called by 
the builders before save.
+       PtypeValidator func(string) error
        // DefaultV0 holds the default value on creation for the "V0" field.
        DefaultV0 string
+       // V0Validator is a validator for the "V0" field. It is called by the 
builders before save.
+       V0Validator func(string) error
        // DefaultV1 holds the default value on creation for the "V1" field.
        DefaultV1 string
+       // V1Validator is a validator for the "V1" field. It is called by the 
builders before save.
+       V1Validator func(string) error
        // DefaultV2 holds the default value on creation for the "V2" field.
        DefaultV2 string
+       // V2Validator is a validator for the "V2" field. It is called by the 
builders before save.
+       V2Validator func(string) error
        // DefaultV3 holds the default value on creation for the "V3" field.
        DefaultV3 string
+       // V3Validator is a validator for the "V3" field. It is called by the 
builders before save.
+       V3Validator func(string) error
        // DefaultV4 holds the default value on creation for the "V4" field.
        DefaultV4 string
+       // V4Validator is a validator for the "V4" field. It is called by the 
builders before save.
+       V4Validator func(string) error
        // DefaultV5 holds the default value on creation for the "V5" field.
        DefaultV5 string
+       // V5Validator is a validator for the "V5" field. It is called by the 
builders before save.
+       V5Validator func(string) error
 )
 
 // OrderOption defines the ordering options for the CasbinRule queries.
diff --git a/ent/casbinrule_create.go b/ent/casbinrule_create.go
index b906adb..0ebdbff 100644
--- a/ent/casbinrule_create.go
+++ b/ent/casbinrule_create.go
@@ -7,6 +7,7 @@ import (
        "errors"
        "fmt"
 
+       "entgo.io/ent/dialect/sql"
        "entgo.io/ent/dialect/sql/sqlgraph"
        "entgo.io/ent/schema/field"
        "github.com/apache/casbin-ent-adapter/ent/casbinrule"
@@ -17,6 +18,7 @@ type CasbinRuleCreate struct {
        config
        mutation *CasbinRuleMutation
        hooks    []Hook
+       conflict []sql.ConflictOption
 }
 
 // SetPtype sets the "Ptype" field.
@@ -187,24 +189,59 @@ func (_c *CasbinRuleCreate) check() error {
        if _, ok := _c.mutation.Ptype(); !ok {
                return &ValidationError{Name: "Ptype", err: errors.New(`ent: 
missing required field "CasbinRule.Ptype"`)}
        }
+       if v, ok := _c.mutation.Ptype(); ok {
+               if err := casbinrule.PtypeValidator(v); err != nil {
+                       return &ValidationError{Name: "Ptype", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.Ptype": %w`, err)}
+               }
+       }
        if _, ok := _c.mutation.V0(); !ok {
                return &ValidationError{Name: "V0", err: errors.New(`ent: 
missing required field "CasbinRule.V0"`)}
        }
+       if v, ok := _c.mutation.V0(); ok {
+               if err := casbinrule.V0Validator(v); err != nil {
+                       return &ValidationError{Name: "V0", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V0": %w`, err)}
+               }
+       }
        if _, ok := _c.mutation.V1(); !ok {
                return &ValidationError{Name: "V1", err: errors.New(`ent: 
missing required field "CasbinRule.V1"`)}
        }
+       if v, ok := _c.mutation.V1(); ok {
+               if err := casbinrule.V1Validator(v); err != nil {
+                       return &ValidationError{Name: "V1", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V1": %w`, err)}
+               }
+       }
        if _, ok := _c.mutation.V2(); !ok {
                return &ValidationError{Name: "V2", err: errors.New(`ent: 
missing required field "CasbinRule.V2"`)}
        }
+       if v, ok := _c.mutation.V2(); ok {
+               if err := casbinrule.V2Validator(v); err != nil {
+                       return &ValidationError{Name: "V2", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V2": %w`, err)}
+               }
+       }
        if _, ok := _c.mutation.V3(); !ok {
                return &ValidationError{Name: "V3", err: errors.New(`ent: 
missing required field "CasbinRule.V3"`)}
        }
+       if v, ok := _c.mutation.V3(); ok {
+               if err := casbinrule.V3Validator(v); err != nil {
+                       return &ValidationError{Name: "V3", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V3": %w`, err)}
+               }
+       }
        if _, ok := _c.mutation.V4(); !ok {
                return &ValidationError{Name: "V4", err: errors.New(`ent: 
missing required field "CasbinRule.V4"`)}
        }
+       if v, ok := _c.mutation.V4(); ok {
+               if err := casbinrule.V4Validator(v); err != nil {
+                       return &ValidationError{Name: "V4", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V4": %w`, err)}
+               }
+       }
        if _, ok := _c.mutation.V5(); !ok {
                return &ValidationError{Name: "V5", err: errors.New(`ent: 
missing required field "CasbinRule.V5"`)}
        }
+       if v, ok := _c.mutation.V5(); ok {
+               if err := casbinrule.V5Validator(v); err != nil {
+                       return &ValidationError{Name: "V5", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V5": %w`, err)}
+               }
+       }
        return nil
 }
 
@@ -231,6 +268,7 @@ func (_c *CasbinRuleCreate) createSpec() (*CasbinRule, 
*sqlgraph.CreateSpec) {
                _node = &CasbinRule{config: _c.config}
                _spec = sqlgraph.NewCreateSpec(casbinrule.Table, 
sqlgraph.NewFieldSpec(casbinrule.FieldID, field.TypeInt))
        )
+       _spec.OnConflict = _c.conflict
        if value, ok := _c.mutation.Ptype(); ok {
                _spec.SetField(casbinrule.FieldPtype, field.TypeString, value)
                _node.Ptype = value
@@ -262,11 +300,316 @@ func (_c *CasbinRuleCreate) createSpec() (*CasbinRule, 
*sqlgraph.CreateSpec) {
        return _node, _spec
 }
 
+// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
+// of the `INSERT` statement. For example:
+//
+//     client.CasbinRule.Create().
+//             SetPtype(v).
+//             OnConflict(
+//                     // Update the row with the new values
+//                     // the was proposed for insertion.
+//                     sql.ResolveWithNewValues(),
+//             ).
+//             // Override some of the fields with custom
+//             // update values.
+//             Update(func(u *ent.CasbinRuleUpsert) {
+//                     SetPtype(v+v).
+//             }).
+//             Exec(ctx)
+func (_c *CasbinRuleCreate) OnConflict(opts ...sql.ConflictOption) 
*CasbinRuleUpsertOne {
+       _c.conflict = opts
+       return &CasbinRuleUpsertOne{
+               create: _c,
+       }
+}
+
+// OnConflictColumns calls `OnConflict` and configures the columns
+// as conflict target. Using this option is equivalent to using:
+//
+//     client.CasbinRule.Create().
+//             OnConflict(sql.ConflictColumns(columns...)).
+//             Exec(ctx)
+func (_c *CasbinRuleCreate) OnConflictColumns(columns ...string) 
*CasbinRuleUpsertOne {
+       _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...))
+       return &CasbinRuleUpsertOne{
+               create: _c,
+       }
+}
+
+type (
+       // CasbinRuleUpsertOne is the builder for "upsert"-ing
+       //  one CasbinRule node.
+       CasbinRuleUpsertOne struct {
+               create *CasbinRuleCreate
+       }
+
+       // CasbinRuleUpsert is the "OnConflict" setter.
+       CasbinRuleUpsert struct {
+               *sql.UpdateSet
+       }
+)
+
+// SetPtype sets the "Ptype" field.
+func (u *CasbinRuleUpsert) SetPtype(v string) *CasbinRuleUpsert {
+       u.Set(casbinrule.FieldPtype, v)
+       return u
+}
+
+// UpdatePtype sets the "Ptype" field to the value that was provided on create.
+func (u *CasbinRuleUpsert) UpdatePtype() *CasbinRuleUpsert {
+       u.SetExcluded(casbinrule.FieldPtype)
+       return u
+}
+
+// SetV0 sets the "V0" field.
+func (u *CasbinRuleUpsert) SetV0(v string) *CasbinRuleUpsert {
+       u.Set(casbinrule.FieldV0, v)
+       return u
+}
+
+// UpdateV0 sets the "V0" field to the value that was provided on create.
+func (u *CasbinRuleUpsert) UpdateV0() *CasbinRuleUpsert {
+       u.SetExcluded(casbinrule.FieldV0)
+       return u
+}
+
+// SetV1 sets the "V1" field.
+func (u *CasbinRuleUpsert) SetV1(v string) *CasbinRuleUpsert {
+       u.Set(casbinrule.FieldV1, v)
+       return u
+}
+
+// UpdateV1 sets the "V1" field to the value that was provided on create.
+func (u *CasbinRuleUpsert) UpdateV1() *CasbinRuleUpsert {
+       u.SetExcluded(casbinrule.FieldV1)
+       return u
+}
+
+// SetV2 sets the "V2" field.
+func (u *CasbinRuleUpsert) SetV2(v string) *CasbinRuleUpsert {
+       u.Set(casbinrule.FieldV2, v)
+       return u
+}
+
+// UpdateV2 sets the "V2" field to the value that was provided on create.
+func (u *CasbinRuleUpsert) UpdateV2() *CasbinRuleUpsert {
+       u.SetExcluded(casbinrule.FieldV2)
+       return u
+}
+
+// SetV3 sets the "V3" field.
+func (u *CasbinRuleUpsert) SetV3(v string) *CasbinRuleUpsert {
+       u.Set(casbinrule.FieldV3, v)
+       return u
+}
+
+// UpdateV3 sets the "V3" field to the value that was provided on create.
+func (u *CasbinRuleUpsert) UpdateV3() *CasbinRuleUpsert {
+       u.SetExcluded(casbinrule.FieldV3)
+       return u
+}
+
+// SetV4 sets the "V4" field.
+func (u *CasbinRuleUpsert) SetV4(v string) *CasbinRuleUpsert {
+       u.Set(casbinrule.FieldV4, v)
+       return u
+}
+
+// UpdateV4 sets the "V4" field to the value that was provided on create.
+func (u *CasbinRuleUpsert) UpdateV4() *CasbinRuleUpsert {
+       u.SetExcluded(casbinrule.FieldV4)
+       return u
+}
+
+// SetV5 sets the "V5" field.
+func (u *CasbinRuleUpsert) SetV5(v string) *CasbinRuleUpsert {
+       u.Set(casbinrule.FieldV5, v)
+       return u
+}
+
+// UpdateV5 sets the "V5" field to the value that was provided on create.
+func (u *CasbinRuleUpsert) UpdateV5() *CasbinRuleUpsert {
+       u.SetExcluded(casbinrule.FieldV5)
+       return u
+}
+
+// UpdateNewValues updates the mutable fields using the new values that were 
set on create.
+// Using this option is equivalent to using:
+//
+//     client.CasbinRule.Create().
+//             OnConflict(
+//                     sql.ResolveWithNewValues(),
+//             ).
+//             Exec(ctx)
+func (u *CasbinRuleUpsertOne) UpdateNewValues() *CasbinRuleUpsertOne {
+       u.create.conflict = append(u.create.conflict, 
sql.ResolveWithNewValues())
+       return u
+}
+
+// Ignore sets each column to itself in case of conflict.
+// Using this option is equivalent to using:
+//
+//     client.CasbinRule.Create().
+//         OnConflict(sql.ResolveWithIgnore()).
+//         Exec(ctx)
+func (u *CasbinRuleUpsertOne) Ignore() *CasbinRuleUpsertOne {
+       u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore())
+       return u
+}
+
+// DoNothing configures the conflict_action to `DO NOTHING`.
+// Supported only by SQLite and PostgreSQL.
+func (u *CasbinRuleUpsertOne) DoNothing() *CasbinRuleUpsertOne {
+       u.create.conflict = append(u.create.conflict, sql.DoNothing())
+       return u
+}
+
+// Update allows overriding fields `UPDATE` values. See the 
CasbinRuleCreate.OnConflict
+// documentation for more info.
+func (u *CasbinRuleUpsertOne) Update(set func(*CasbinRuleUpsert)) 
*CasbinRuleUpsertOne {
+       u.create.conflict = append(u.create.conflict, 
sql.ResolveWith(func(update *sql.UpdateSet) {
+               set(&CasbinRuleUpsert{UpdateSet: update})
+       }))
+       return u
+}
+
+// SetPtype sets the "Ptype" field.
+func (u *CasbinRuleUpsertOne) SetPtype(v string) *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetPtype(v)
+       })
+}
+
+// UpdatePtype sets the "Ptype" field to the value that was provided on create.
+func (u *CasbinRuleUpsertOne) UpdatePtype() *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdatePtype()
+       })
+}
+
+// SetV0 sets the "V0" field.
+func (u *CasbinRuleUpsertOne) SetV0(v string) *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV0(v)
+       })
+}
+
+// UpdateV0 sets the "V0" field to the value that was provided on create.
+func (u *CasbinRuleUpsertOne) UpdateV0() *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV0()
+       })
+}
+
+// SetV1 sets the "V1" field.
+func (u *CasbinRuleUpsertOne) SetV1(v string) *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV1(v)
+       })
+}
+
+// UpdateV1 sets the "V1" field to the value that was provided on create.
+func (u *CasbinRuleUpsertOne) UpdateV1() *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV1()
+       })
+}
+
+// SetV2 sets the "V2" field.
+func (u *CasbinRuleUpsertOne) SetV2(v string) *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV2(v)
+       })
+}
+
+// UpdateV2 sets the "V2" field to the value that was provided on create.
+func (u *CasbinRuleUpsertOne) UpdateV2() *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV2()
+       })
+}
+
+// SetV3 sets the "V3" field.
+func (u *CasbinRuleUpsertOne) SetV3(v string) *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV3(v)
+       })
+}
+
+// UpdateV3 sets the "V3" field to the value that was provided on create.
+func (u *CasbinRuleUpsertOne) UpdateV3() *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV3()
+       })
+}
+
+// SetV4 sets the "V4" field.
+func (u *CasbinRuleUpsertOne) SetV4(v string) *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV4(v)
+       })
+}
+
+// UpdateV4 sets the "V4" field to the value that was provided on create.
+func (u *CasbinRuleUpsertOne) UpdateV4() *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV4()
+       })
+}
+
+// SetV5 sets the "V5" field.
+func (u *CasbinRuleUpsertOne) SetV5(v string) *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV5(v)
+       })
+}
+
+// UpdateV5 sets the "V5" field to the value that was provided on create.
+func (u *CasbinRuleUpsertOne) UpdateV5() *CasbinRuleUpsertOne {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV5()
+       })
+}
+
+// Exec executes the query.
+func (u *CasbinRuleUpsertOne) Exec(ctx context.Context) error {
+       if len(u.create.conflict) == 0 {
+               return errors.New("ent: missing options for 
CasbinRuleCreate.OnConflict")
+       }
+       return u.create.Exec(ctx)
+}
+
+// ExecX is like Exec, but panics if an error occurs.
+func (u *CasbinRuleUpsertOne) ExecX(ctx context.Context) {
+       if err := u.create.Exec(ctx); err != nil {
+               panic(err)
+       }
+}
+
+// Exec executes the UPSERT query and returns the inserted/updated ID.
+func (u *CasbinRuleUpsertOne) ID(ctx context.Context) (id int, err error) {
+       node, err := u.create.Save(ctx)
+       if err != nil {
+               return id, err
+       }
+       return node.ID, nil
+}
+
+// IDX is like ID, but panics if an error occurs.
+func (u *CasbinRuleUpsertOne) IDX(ctx context.Context) int {
+       id, err := u.ID(ctx)
+       if err != nil {
+               panic(err)
+       }
+       return id
+}
+
 // CasbinRuleCreateBulk is the builder for creating many CasbinRule entities 
in bulk.
 type CasbinRuleCreateBulk struct {
        config
        err      error
        builders []*CasbinRuleCreate
+       conflict []sql.ConflictOption
 }
 
 // Save creates the CasbinRule entities in the database.
@@ -296,6 +639,7 @@ func (_c *CasbinRuleCreateBulk) Save(ctx context.Context) 
([]*CasbinRule, error)
                                        _, err = mutators[i+1].Mutate(root, 
_c.builders[i+1].mutation)
                                } else {
                                        spec := 
&sqlgraph.BatchCreateSpec{Nodes: specs}
+                                       spec.OnConflict = _c.conflict
                                        // Invoke the actual operation on the 
latest mutation in the chain.
                                        if err = sqlgraph.BatchCreate(ctx, 
_c.driver, spec); err != nil {
                                                if 
sqlgraph.IsConstraintError(err) {
@@ -349,3 +693,205 @@ func (_c *CasbinRuleCreateBulk) ExecX(ctx 
context.Context) {
                panic(err)
        }
 }
+
+// OnConflict allows configuring the `ON CONFLICT` / `ON DUPLICATE KEY` clause
+// of the `INSERT` statement. For example:
+//
+//     client.CasbinRule.CreateBulk(builders...).
+//             OnConflict(
+//                     // Update the row with the new values
+//                     // the was proposed for insertion.
+//                     sql.ResolveWithNewValues(),
+//             ).
+//             // Override some of the fields with custom
+//             // update values.
+//             Update(func(u *ent.CasbinRuleUpsert) {
+//                     SetPtype(v+v).
+//             }).
+//             Exec(ctx)
+func (_c *CasbinRuleCreateBulk) OnConflict(opts ...sql.ConflictOption) 
*CasbinRuleUpsertBulk {
+       _c.conflict = opts
+       return &CasbinRuleUpsertBulk{
+               create: _c,
+       }
+}
+
+// OnConflictColumns calls `OnConflict` and configures the columns
+// as conflict target. Using this option is equivalent to using:
+//
+//     client.CasbinRule.Create().
+//             OnConflict(sql.ConflictColumns(columns...)).
+//             Exec(ctx)
+func (_c *CasbinRuleCreateBulk) OnConflictColumns(columns ...string) 
*CasbinRuleUpsertBulk {
+       _c.conflict = append(_c.conflict, sql.ConflictColumns(columns...))
+       return &CasbinRuleUpsertBulk{
+               create: _c,
+       }
+}
+
+// CasbinRuleUpsertBulk is the builder for "upsert"-ing
+// a bulk of CasbinRule nodes.
+type CasbinRuleUpsertBulk struct {
+       create *CasbinRuleCreateBulk
+}
+
+// UpdateNewValues updates the mutable fields using the new values that
+// were set on create. Using this option is equivalent to using:
+//
+//     client.CasbinRule.Create().
+//             OnConflict(
+//                     sql.ResolveWithNewValues(),
+//             ).
+//             Exec(ctx)
+func (u *CasbinRuleUpsertBulk) UpdateNewValues() *CasbinRuleUpsertBulk {
+       u.create.conflict = append(u.create.conflict, 
sql.ResolveWithNewValues())
+       return u
+}
+
+// Ignore sets each column to itself in case of conflict.
+// Using this option is equivalent to using:
+//
+//     client.CasbinRule.Create().
+//             OnConflict(sql.ResolveWithIgnore()).
+//             Exec(ctx)
+func (u *CasbinRuleUpsertBulk) Ignore() *CasbinRuleUpsertBulk {
+       u.create.conflict = append(u.create.conflict, sql.ResolveWithIgnore())
+       return u
+}
+
+// DoNothing configures the conflict_action to `DO NOTHING`.
+// Supported only by SQLite and PostgreSQL.
+func (u *CasbinRuleUpsertBulk) DoNothing() *CasbinRuleUpsertBulk {
+       u.create.conflict = append(u.create.conflict, sql.DoNothing())
+       return u
+}
+
+// Update allows overriding fields `UPDATE` values. See the 
CasbinRuleCreateBulk.OnConflict
+// documentation for more info.
+func (u *CasbinRuleUpsertBulk) Update(set func(*CasbinRuleUpsert)) 
*CasbinRuleUpsertBulk {
+       u.create.conflict = append(u.create.conflict, 
sql.ResolveWith(func(update *sql.UpdateSet) {
+               set(&CasbinRuleUpsert{UpdateSet: update})
+       }))
+       return u
+}
+
+// SetPtype sets the "Ptype" field.
+func (u *CasbinRuleUpsertBulk) SetPtype(v string) *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetPtype(v)
+       })
+}
+
+// UpdatePtype sets the "Ptype" field to the value that was provided on create.
+func (u *CasbinRuleUpsertBulk) UpdatePtype() *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdatePtype()
+       })
+}
+
+// SetV0 sets the "V0" field.
+func (u *CasbinRuleUpsertBulk) SetV0(v string) *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV0(v)
+       })
+}
+
+// UpdateV0 sets the "V0" field to the value that was provided on create.
+func (u *CasbinRuleUpsertBulk) UpdateV0() *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV0()
+       })
+}
+
+// SetV1 sets the "V1" field.
+func (u *CasbinRuleUpsertBulk) SetV1(v string) *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV1(v)
+       })
+}
+
+// UpdateV1 sets the "V1" field to the value that was provided on create.
+func (u *CasbinRuleUpsertBulk) UpdateV1() *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV1()
+       })
+}
+
+// SetV2 sets the "V2" field.
+func (u *CasbinRuleUpsertBulk) SetV2(v string) *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV2(v)
+       })
+}
+
+// UpdateV2 sets the "V2" field to the value that was provided on create.
+func (u *CasbinRuleUpsertBulk) UpdateV2() *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV2()
+       })
+}
+
+// SetV3 sets the "V3" field.
+func (u *CasbinRuleUpsertBulk) SetV3(v string) *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV3(v)
+       })
+}
+
+// UpdateV3 sets the "V3" field to the value that was provided on create.
+func (u *CasbinRuleUpsertBulk) UpdateV3() *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV3()
+       })
+}
+
+// SetV4 sets the "V4" field.
+func (u *CasbinRuleUpsertBulk) SetV4(v string) *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV4(v)
+       })
+}
+
+// UpdateV4 sets the "V4" field to the value that was provided on create.
+func (u *CasbinRuleUpsertBulk) UpdateV4() *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV4()
+       })
+}
+
+// SetV5 sets the "V5" field.
+func (u *CasbinRuleUpsertBulk) SetV5(v string) *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.SetV5(v)
+       })
+}
+
+// UpdateV5 sets the "V5" field to the value that was provided on create.
+func (u *CasbinRuleUpsertBulk) UpdateV5() *CasbinRuleUpsertBulk {
+       return u.Update(func(s *CasbinRuleUpsert) {
+               s.UpdateV5()
+       })
+}
+
+// Exec executes the query.
+func (u *CasbinRuleUpsertBulk) Exec(ctx context.Context) error {
+       if u.create.err != nil {
+               return u.create.err
+       }
+       for i, b := range u.create.builders {
+               if len(b.conflict) != 0 {
+                       return fmt.Errorf("ent: OnConflict was set for builder 
%d. Set it on the CasbinRuleCreateBulk instead", i)
+               }
+       }
+       if len(u.create.conflict) == 0 {
+               return errors.New("ent: missing options for 
CasbinRuleCreateBulk.OnConflict")
+       }
+       return u.create.Exec(ctx)
+}
+
+// ExecX is like Exec, but panics if an error occurs.
+func (u *CasbinRuleUpsertBulk) ExecX(ctx context.Context) {
+       if err := u.create.Exec(ctx); err != nil {
+               panic(err)
+       }
+}
diff --git a/ent/casbinrule_update.go b/ent/casbinrule_update.go
index e54e7a3..eea3ef6 100644
--- a/ent/casbinrule_update.go
+++ b/ent/casbinrule_update.go
@@ -157,7 +157,50 @@ func (_u *CasbinRuleUpdate) ExecX(ctx context.Context) {
        }
 }
 
+// check runs all checks and user-defined validators on the builder.
+func (_u *CasbinRuleUpdate) check() error {
+       if v, ok := _u.mutation.Ptype(); ok {
+               if err := casbinrule.PtypeValidator(v); err != nil {
+                       return &ValidationError{Name: "Ptype", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.Ptype": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V0(); ok {
+               if err := casbinrule.V0Validator(v); err != nil {
+                       return &ValidationError{Name: "V0", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V0": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V1(); ok {
+               if err := casbinrule.V1Validator(v); err != nil {
+                       return &ValidationError{Name: "V1", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V1": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V2(); ok {
+               if err := casbinrule.V2Validator(v); err != nil {
+                       return &ValidationError{Name: "V2", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V2": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V3(); ok {
+               if err := casbinrule.V3Validator(v); err != nil {
+                       return &ValidationError{Name: "V3", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V3": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V4(); ok {
+               if err := casbinrule.V4Validator(v); err != nil {
+                       return &ValidationError{Name: "V4", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V4": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V5(); ok {
+               if err := casbinrule.V5Validator(v); err != nil {
+                       return &ValidationError{Name: "V5", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V5": %w`, err)}
+               }
+       }
+       return nil
+}
+
 func (_u *CasbinRuleUpdate) sqlSave(ctx context.Context) (_node int, err 
error) {
+       if err := _u.check(); err != nil {
+               return _node, err
+       }
        _spec := sqlgraph.NewUpdateSpec(casbinrule.Table, casbinrule.Columns, 
sqlgraph.NewFieldSpec(casbinrule.FieldID, field.TypeInt))
        if ps := _u.mutation.predicates; len(ps) > 0 {
                _spec.Predicate = func(selector *sql.Selector) {
@@ -350,7 +393,50 @@ func (_u *CasbinRuleUpdateOne) ExecX(ctx context.Context) {
        }
 }
 
+// check runs all checks and user-defined validators on the builder.
+func (_u *CasbinRuleUpdateOne) check() error {
+       if v, ok := _u.mutation.Ptype(); ok {
+               if err := casbinrule.PtypeValidator(v); err != nil {
+                       return &ValidationError{Name: "Ptype", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.Ptype": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V0(); ok {
+               if err := casbinrule.V0Validator(v); err != nil {
+                       return &ValidationError{Name: "V0", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V0": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V1(); ok {
+               if err := casbinrule.V1Validator(v); err != nil {
+                       return &ValidationError{Name: "V1", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V1": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V2(); ok {
+               if err := casbinrule.V2Validator(v); err != nil {
+                       return &ValidationError{Name: "V2", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V2": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V3(); ok {
+               if err := casbinrule.V3Validator(v); err != nil {
+                       return &ValidationError{Name: "V3", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V3": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V4(); ok {
+               if err := casbinrule.V4Validator(v); err != nil {
+                       return &ValidationError{Name: "V4", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V4": %w`, err)}
+               }
+       }
+       if v, ok := _u.mutation.V5(); ok {
+               if err := casbinrule.V5Validator(v); err != nil {
+                       return &ValidationError{Name: "V5", err: 
fmt.Errorf(`ent: validator failed for field "CasbinRule.V5": %w`, err)}
+               }
+       }
+       return nil
+}
+
 func (_u *CasbinRuleUpdateOne) sqlSave(ctx context.Context) (_node 
*CasbinRule, err error) {
+       if err := _u.check(); err != nil {
+               return _node, err
+       }
        _spec := sqlgraph.NewUpdateSpec(casbinrule.Table, casbinrule.Columns, 
sqlgraph.NewFieldSpec(casbinrule.FieldID, field.TypeInt))
        id, ok := _u.mutation.ID()
        if !ok {
diff --git a/ent/generate.go b/ent/generate.go
index 8d3fdfd..6affc35 100644
--- a/ent/generate.go
+++ b/ent/generate.go
@@ -1,3 +1,5 @@
 package ent
 
-//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
+// The sql/upsert feature generates the OnConflict builders the adapter uses to
+// make policy inserts idempotent against the unique index on (ptype, v0..v5).
+//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature 
sql/upsert ./schema
diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go
index 4e3cd0e..0e980df 100644
--- a/ent/migrate/schema.go
+++ b/ent/migrate/schema.go
@@ -11,19 +11,26 @@ var (
        // CasbinRulesColumns holds the columns for the "casbin_rules" table.
        CasbinRulesColumns = []*schema.Column{
                {Name: "id", Type: field.TypeInt, Increment: true},
-               {Name: "ptype", Type: field.TypeString, Default: ""},
-               {Name: "v0", Type: field.TypeString, Default: ""},
-               {Name: "v1", Type: field.TypeString, Default: ""},
-               {Name: "v2", Type: field.TypeString, Default: ""},
-               {Name: "v3", Type: field.TypeString, Default: ""},
-               {Name: "v4", Type: field.TypeString, Default: ""},
-               {Name: "v5", Type: field.TypeString, Default: ""},
+               {Name: "ptype", Type: field.TypeString, Size: 100, Default: ""},
+               {Name: "v0", Type: field.TypeString, Size: 100, Default: ""},
+               {Name: "v1", Type: field.TypeString, Size: 100, Default: ""},
+               {Name: "v2", Type: field.TypeString, Size: 100, Default: ""},
+               {Name: "v3", Type: field.TypeString, Size: 100, Default: ""},
+               {Name: "v4", Type: field.TypeString, Size: 100, Default: ""},
+               {Name: "v5", Type: field.TypeString, Size: 100, Default: ""},
        }
        // CasbinRulesTable holds the schema information for the "casbin_rules" 
table.
        CasbinRulesTable = &schema.Table{
                Name:       "casbin_rules",
                Columns:    CasbinRulesColumns,
                PrimaryKey: []*schema.Column{CasbinRulesColumns[0]},
+               Indexes: []*schema.Index{
+                       {
+                               Name:    "casbinrule_ptype_v0_v1_v2_v3_v4_v5",
+                               Unique:  true,
+                               Columns: 
[]*schema.Column{CasbinRulesColumns[1], CasbinRulesColumns[2], 
CasbinRulesColumns[3], CasbinRulesColumns[4], CasbinRulesColumns[5], 
CasbinRulesColumns[6], CasbinRulesColumns[7]},
+                       },
+               },
        }
        // Tables holds all the tables in the schema.
        Tables = []*schema.Table{
diff --git a/ent/runtime.go b/ent/runtime.go
index d893770..0be8060 100644
--- a/ent/runtime.go
+++ b/ent/runtime.go
@@ -17,28 +17,42 @@ func init() {
        casbinruleDescPtype := casbinruleFields[0].Descriptor()
        // casbinrule.DefaultPtype holds the default value on creation for the 
Ptype field.
        casbinrule.DefaultPtype = casbinruleDescPtype.Default.(string)
+       // casbinrule.PtypeValidator is a validator for the "Ptype" field. It 
is called by the builders before save.
+       casbinrule.PtypeValidator = 
casbinruleDescPtype.Validators[0].(func(string) error)
        // casbinruleDescV0 is the schema descriptor for V0 field.
        casbinruleDescV0 := casbinruleFields[1].Descriptor()
        // casbinrule.DefaultV0 holds the default value on creation for the V0 
field.
        casbinrule.DefaultV0 = casbinruleDescV0.Default.(string)
+       // casbinrule.V0Validator is a validator for the "V0" field. It is 
called by the builders before save.
+       casbinrule.V0Validator = casbinruleDescV0.Validators[0].(func(string) 
error)
        // casbinruleDescV1 is the schema descriptor for V1 field.
        casbinruleDescV1 := casbinruleFields[2].Descriptor()
        // casbinrule.DefaultV1 holds the default value on creation for the V1 
field.
        casbinrule.DefaultV1 = casbinruleDescV1.Default.(string)
+       // casbinrule.V1Validator is a validator for the "V1" field. It is 
called by the builders before save.
+       casbinrule.V1Validator = casbinruleDescV1.Validators[0].(func(string) 
error)
        // casbinruleDescV2 is the schema descriptor for V2 field.
        casbinruleDescV2 := casbinruleFields[3].Descriptor()
        // casbinrule.DefaultV2 holds the default value on creation for the V2 
field.
        casbinrule.DefaultV2 = casbinruleDescV2.Default.(string)
+       // casbinrule.V2Validator is a validator for the "V2" field. It is 
called by the builders before save.
+       casbinrule.V2Validator = casbinruleDescV2.Validators[0].(func(string) 
error)
        // casbinruleDescV3 is the schema descriptor for V3 field.
        casbinruleDescV3 := casbinruleFields[4].Descriptor()
        // casbinrule.DefaultV3 holds the default value on creation for the V3 
field.
        casbinrule.DefaultV3 = casbinruleDescV3.Default.(string)
+       // casbinrule.V3Validator is a validator for the "V3" field. It is 
called by the builders before save.
+       casbinrule.V3Validator = casbinruleDescV3.Validators[0].(func(string) 
error)
        // casbinruleDescV4 is the schema descriptor for V4 field.
        casbinruleDescV4 := casbinruleFields[5].Descriptor()
        // casbinrule.DefaultV4 holds the default value on creation for the V4 
field.
        casbinrule.DefaultV4 = casbinruleDescV4.Default.(string)
+       // casbinrule.V4Validator is a validator for the "V4" field. It is 
called by the builders before save.
+       casbinrule.V4Validator = casbinruleDescV4.Validators[0].(func(string) 
error)
        // casbinruleDescV5 is the schema descriptor for V5 field.
        casbinruleDescV5 := casbinruleFields[6].Descriptor()
        // casbinrule.DefaultV5 holds the default value on creation for the V5 
field.
        casbinrule.DefaultV5 = casbinruleDescV5.Default.(string)
+       // casbinrule.V5Validator is a validator for the "V5" field. It is 
called by the builders before save.
+       casbinrule.V5Validator = casbinruleDescV5.Validators[0].(func(string) 
error)
 }
diff --git a/ent/schema/casbinrule.go b/ent/schema/casbinrule.go
index 4c87380..497c2ef 100644
--- a/ent/schema/casbinrule.go
+++ b/ent/schema/casbinrule.go
@@ -6,21 +6,37 @@ import (
        "entgo.io/ent/schema/index"
 )
 
+// MaxFieldLen is the maximum length of every policy column.
+//
+// The unique index below spans all seven policy columns. MySQL caps a single
+// InnoDB index key at 3072 bytes, and ent creates its tables as utf8mb4, where
+// a character costs up to 4 bytes. Leaving the columns at ent's default
+// varchar(255) would need 7*255*4 = 7140 bytes and the index would be rejected
+// with "Error 1071: Specified key was too long". At 100 the key needs
+// 7*100*4 = 2800 bytes, which fits, and it matches the varchar(100) used by
+// casbin's canonical SQL schema and by gorm-adapter.
+const MaxFieldLen = 100
+
 // CasbinRule holds the schema definition for the CasbinRule entity.
 type CasbinRule struct {
        ent.Schema
 }
 
 // Fields of the CasbinRule.
+//
+// Every column is NOT NULL with an empty-string default. That matters for the
+// unique index: SQL treats NULLs as distinct from each other, so nullable
+// columns would silently defeat the constraint for every rule that leaves some
+// of V0..V5 unused.
 func (CasbinRule) Fields() []ent.Field {
        return []ent.Field{
-               field.String("Ptype").Default(""),
-               field.String("V0").Default(""),
-               field.String("V1").Default(""),
-               field.String("V2").Default(""),
-               field.String("V3").Default(""),
-               field.String("V4").Default(""),
-               field.String("V5").Default(""),
+               field.String("Ptype").MaxLen(MaxFieldLen).Default(""),
+               field.String("V0").MaxLen(MaxFieldLen).Default(""),
+               field.String("V1").MaxLen(MaxFieldLen).Default(""),
+               field.String("V2").MaxLen(MaxFieldLen).Default(""),
+               field.String("V3").MaxLen(MaxFieldLen).Default(""),
+               field.String("V4").MaxLen(MaxFieldLen).Default(""),
+               field.String("V5").MaxLen(MaxFieldLen).Default(""),
        }
 }
 
@@ -29,7 +45,12 @@ func (CasbinRule) Edges() []ent.Edge {
        return nil
 }
 
-func (CasbinRule) Index() []ent.Index {
+// Indexes of the CasbinRule.
+//
+// The method must be named Indexes: ent discovers schema indexes through this
+// exact interface method, so a differently named method compiles fine and is
+// silently ignored by codegen.
+func (CasbinRule) Indexes() []ent.Index {
        return []ent.Index{
                index.Fields("Ptype", "V0", "V1", "V2", "V3", "V4", 
"V5").Unique(),
        }
diff --git a/go.sum b/go.sum
index 0d2d2ab..62f6047 100644
--- a/go.sum
+++ b/go.sum
@@ -35,6 +35,8 @@ github.com/google/uuid v1.6.0 
h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod 
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/hashicorp/hcl/v2 v2.24.0 
h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
 github.com/hashicorp/hcl/v2 v2.24.0/go.mod 
h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
+github.com/inconshreveable/mousetrap v1.1.0 
h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod 
h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/jackc/pgpassfile v1.0.0 
h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
 github.com/jackc/pgpassfile v1.0.0/go.mod 
h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 
h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -49,16 +51,24 @@ github.com/kr/text v0.2.0 
h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod 
h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
 github.com/lib/pq v1.10.9/go.mod 
h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-runewidth v0.0.9 
h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
+github.com/mattn/go-runewidth v0.0.9/go.mod 
h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-sqlite3 v1.14.28 
h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
 github.com/mattn/go-sqlite3 v1.14.28/go.mod 
h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 github.com/mitchellh/go-wordwrap v1.0.1 
h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 github.com/mitchellh/go-wordwrap v1.0.1/go.mod 
h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/olekukonko/tablewriter v0.0.5 
h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
+github.com/olekukonko/tablewriter v0.0.5/go.mod 
h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod 
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.12.0 
h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod 
h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod 
h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod 
h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/stretchr/objx v0.1.0/go.mod 
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0/go.mod 
h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to