test: expand coverage across readers, writers, cmd, ui, diff and merge
Implements tests/_plans and previously deferred packages; updates plan README with new coverage numbers.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package bun
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
)
|
||||
|
||||
func newTestReader() *Reader { return NewReader(&readers.ReaderOptions{}) }
|
||||
|
||||
func mustExpr(t *testing.T, src string) ast.Expr {
|
||||
t.Helper()
|
||||
e, err := parser.ParseExpr(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func TestGoTypeToSQL(t *testing.T) {
|
||||
r := newTestReader()
|
||||
tests := []struct{ src, want string }{
|
||||
{"int", "integer"}, {"int32", "integer"}, {"int64", "bigint"},
|
||||
{"string", "text"}, {"bool", "boolean"}, {"float32", "real"},
|
||||
{"float64", "double precision"}, {"uint8", "text"},
|
||||
{"time.Time", "timestamp"}, {"time.Duration", "text"},
|
||||
{"sql_types.SqlString", "text"}, {"sql_types.SqlInt", "integer"},
|
||||
{"sql_types.SqlInt64", "bigint"}, {"sql_types.SqlFloat", "double precision"},
|
||||
{"sql_types.SqlBool", "boolean"}, {"sql_types.SqlTime", "timestamp"},
|
||||
{"sql_types.Other", "text"}, {"other.Thing", "text"},
|
||||
{"*int64", "bigint"}, {"*time.Time", "timestamp"}, {"[]byte", "text"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.src, func(t *testing.T) {
|
||||
if got := r.goTypeToSQL(mustExpr(t, tt.src)); got != tt.want {
|
||||
t.Errorf("got %q want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveTableName(t *testing.T) {
|
||||
r := newTestReader()
|
||||
for in, want := range map[string]string{
|
||||
"ModelUser": "user",
|
||||
"ModelUserRole": "user_role",
|
||||
"Account": "account",
|
||||
"OrderItem": "order_item",
|
||||
} {
|
||||
if got := r.deriveTableName(in); got != want {
|
||||
t.Errorf("%q: got %q want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetReceiverType(t *testing.T) {
|
||||
r := newTestReader()
|
||||
for src, want := range map[string]string{"User": "User", "*User": "User", "*pkg.User": "", "[]User": ""} {
|
||||
if got := r.getReceiverType(mustExpr(t, src)); got != want {
|
||||
t.Errorf("%s: got %q want %q", src, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRelationType(t *testing.T) {
|
||||
r := newTestReader()
|
||||
for tag, want := range map[string]string{
|
||||
`bun:"rel:has-many,join:id=user_id"`: "has-many",
|
||||
`bun:"rel:belongs-to,join:user_id=id"`: "belongs-to",
|
||||
`bun:"rel:has-one,join:id=user_id"`: "has-one",
|
||||
`bun:"rel:many-to-many,join_table:x"`: "many-to-many",
|
||||
`bun:"rel:unknown"`: "",
|
||||
`bun:"id,pk"`: "",
|
||||
} {
|
||||
if got := r.getRelationType(tag); got != want {
|
||||
t.Errorf("%s: got %q want %q", tag, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTableNameMethod(t *testing.T) {
|
||||
r := newTestReader()
|
||||
parse := func(src string) *ast.FuncDecl {
|
||||
f, err := parser.ParseFile(token.NewFileSet(), "x.go", "package p\n"+src, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return f.Decls[0].(*ast.FuncDecl)
|
||||
}
|
||||
if tbl, sch := r.parseTableNameMethod(parse(`func (User) TableName() string { return "public.users" }`)); tbl != "users" || sch != "public" {
|
||||
t.Errorf("qualified: %q %q", tbl, sch)
|
||||
}
|
||||
if tbl, sch := r.parseTableNameMethod(parse(`func (User) TableName() string { return "users" }`)); tbl != "users" || sch != "public" {
|
||||
t.Errorf("plain: %q %q", tbl, sch)
|
||||
}
|
||||
if tbl, _ := r.parseTableNameMethod(parse(`func (User) TableName() string`)); tbl != "" {
|
||||
t.Errorf("no body: %q", tbl)
|
||||
}
|
||||
if tbl, _ := r.parseTableNameMethod(parse(`func (User) TableName() string { x := 1; _ = x; return foo() }`)); tbl != "" {
|
||||
t.Errorf("non-literal: %q", tbl)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package drizzle
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
)
|
||||
|
||||
const fixture = "../../../tests/assets/drizzle/schema.ts"
|
||||
|
||||
func readFile(t *testing.T, path string) *models.Database {
|
||||
t.Helper()
|
||||
db, err := NewReader(&readers.ReaderOptions{FilePath: path}).ReadDatabase()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func findTable(db *models.Database, name string) *models.Table {
|
||||
for _, s := range db.Schemas {
|
||||
for _, tb := range s.Tables {
|
||||
if tb.Name == name {
|
||||
return tb
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestReadFixture(t *testing.T) {
|
||||
db := readFile(t, fixture)
|
||||
if len(db.Schemas) == 0 || len(db.Schemas[0].Tables) == 0 {
|
||||
t.Fatal("expected tables")
|
||||
}
|
||||
if len(db.Schemas[0].Enums) != 1 || db.Schemas[0].Enums[0].Name != "Role" {
|
||||
t.Fatalf("enums = %+v", db.Schemas[0].Enums)
|
||||
}
|
||||
var found bool
|
||||
for _, tb := range db.Schemas[0].Tables {
|
||||
if c, ok := tb.Columns["role"]; ok {
|
||||
found = true
|
||||
if c.Type != "Role" {
|
||||
t.Errorf("role type = %q", c.Type)
|
||||
}
|
||||
}
|
||||
for n := range tb.Columns {
|
||||
if n == "profile" {
|
||||
t.Errorf("relation field leaked as column in %s", tb.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("no role column")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumColumnSyntax(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
}{
|
||||
{"enum constant", "export const role = pgEnum('Role', ['A','B']);\nexport const users = pgTable('users', {\n role: role('role').notNull(),\n});\n"},
|
||||
{"legacy", "export const role = pgEnum('Role', ['A','B']);\nexport const users = pgTable('users', {\n role: pgEnum('Role')('role').notNull(),\n});\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := filepath.Join(t.TempDir(), "s.ts")
|
||||
if err := os.WriteFile(p, []byte(tt.src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tb := findTable(readFile(t, p), "users")
|
||||
if tb == nil {
|
||||
t.Fatal("users missing")
|
||||
}
|
||||
c := tb.Columns["role"]
|
||||
if c == nil || c.Type != "Role" || !c.NotNull {
|
||||
t.Errorf("column = %+v", c)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadDirectorySeparateEnums(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
files := map[string]string{
|
||||
"enums.ts": "export const status = pgEnum('Status', ['on','off']);\n",
|
||||
"tables.ts": "export const items = pgTable('items', {\n status: status('status'),\n});\n",
|
||||
}
|
||||
for n, c := range files {
|
||||
if err := os.WriteFile(filepath.Join(dir, n), []byte(c), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
tb := findTable(readFile(t, dir), "items")
|
||||
if tb == nil {
|
||||
t.Fatal("items missing")
|
||||
}
|
||||
if c := tb.Columns["status"]; c == nil || c.Type != "Status" {
|
||||
t.Errorf("column = %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReaderErrors(t *testing.T) {
|
||||
if _, err := NewReader(&readers.ReaderOptions{}).ReadDatabase(); err == nil {
|
||||
t.Error("expected error for empty path")
|
||||
}
|
||||
if _, err := NewReader(&readers.ReaderOptions{FilePath: "/nonexistent.ts"}).ReadDatabase(); err == nil {
|
||||
t.Error("expected error for missing file")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package gorm
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
)
|
||||
|
||||
func newTestReader() *Reader { return NewReader(&readers.ReaderOptions{}) }
|
||||
|
||||
func mustExpr(t *testing.T, src string) ast.Expr {
|
||||
t.Helper()
|
||||
e, err := parser.ParseExpr(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func TestGoTypeToSQL(t *testing.T) {
|
||||
r := newTestReader()
|
||||
tests := []struct{ src, want string }{
|
||||
{"int", "integer"}, {"int32", "integer"}, {"int64", "bigint"},
|
||||
{"string", "text"}, {"bool", "boolean"}, {"float32", "real"},
|
||||
{"float64", "double precision"}, {"uint8", "text"},
|
||||
{"time.Time", "timestamp"}, {"time.Duration", "text"},
|
||||
{"sql_types.SqlString", "text"}, {"sql_types.SqlInt", "integer"},
|
||||
{"sql_types.SqlInt64", "bigint"}, {"sql_types.SqlFloat", "double precision"},
|
||||
{"sql_types.SqlBool", "boolean"}, {"sql_types.SqlTime", "timestamp"},
|
||||
{"sql_types.Other", "text"}, {"other.Thing", "text"},
|
||||
{"*int64", "bigint"}, {"*time.Time", "timestamp"}, {"[]byte", "text"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.src, func(t *testing.T) {
|
||||
if got := r.goTypeToSQL(mustExpr(t, tt.src)); got != tt.want {
|
||||
t.Errorf("got %q want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldNameToColumnName(t *testing.T) {
|
||||
r := newTestReader()
|
||||
for in, want := range map[string]string{"ID": "i_d", "UserName": "user_name", "name": "name", "": ""} {
|
||||
if got := r.fieldNameToColumnName(in); got != want {
|
||||
t.Errorf("%q: got %q want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetReceiverType(t *testing.T) {
|
||||
r := newTestReader()
|
||||
tests := []struct{ src, want string }{
|
||||
{"User", "User"}, {"*User", "User"}, {"*pkg.User", ""}, {"[]User", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := r.getReceiverType(mustExpr(t, tt.src)); got != tt.want {
|
||||
t.Errorf("%s: got %q want %q", tt.src, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsGORMModel(t *testing.T) {
|
||||
r := newTestReader()
|
||||
tests := []struct {
|
||||
name string
|
||||
field *ast.Field
|
||||
want bool
|
||||
}{
|
||||
{"embedded gorm.Model", &ast.Field{Type: mustExpr(t, "gorm.Model")}, true},
|
||||
{"named field", &ast.Field{Names: []*ast.Ident{ast.NewIdent("M")}, Type: mustExpr(t, "gorm.Model")}, false},
|
||||
{"plain ident", &ast.Field{Type: mustExpr(t, "Model")}, false},
|
||||
{"other package", &ast.Field{Type: mustExpr(t, "other.Model")}, false},
|
||||
{"gorm other", &ast.Field{Type: mustExpr(t, "gorm.DB")}, false},
|
||||
{"non-ident selector base", &ast.Field{Type: mustExpr(t, "a.b.Model")}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := r.isGORMModel(tt.field); got != tt.want {
|
||||
t.Errorf("got %v want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTypeWithReferences(t *testing.T) {
|
||||
r := newTestReader()
|
||||
tests := []struct {
|
||||
in string
|
||||
base string
|
||||
length int
|
||||
refInfo string
|
||||
}{
|
||||
{"bigint", "bigint", 0, ""},
|
||||
{"varchar(50)", "varchar", 50, ""},
|
||||
{"bigint references mainaccount(id) ON DELETE CASCADE", "bigint", 0, "mainaccount(id) ON DELETE CASCADE"},
|
||||
{"varchar(20) REFERENCES t(c)", "varchar", 20, "t(c)"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
base, length, ref := r.parseTypeWithReferences(tt.in)
|
||||
if base != tt.base || length != tt.length || ref != tt.refInfo {
|
||||
t.Errorf("%q: got (%q,%d,%q)", tt.in, base, length, ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateInlineReferenceConstraint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ref string
|
||||
wantNone bool
|
||||
schema string
|
||||
table string
|
||||
col string
|
||||
onDelete string
|
||||
onUpdate string
|
||||
}{
|
||||
{"simple", "accounts(id)", false, "public", "accounts", "id", "NO ACTION", "NO ACTION"},
|
||||
{"schema qualified", "billing.accounts(id)", false, "billing", "accounts", "id", "NO ACTION", "NO ACTION"},
|
||||
{"cascade restrict", "accounts(id) ON DELETE CASCADE ON UPDATE RESTRICT", false, "public", "accounts", "id", "CASCADE", "RESTRICT"},
|
||||
{"set null no action", "accounts(id) on delete set null on update no action", false, "public", "accounts", "id", "SET NULL", "NO ACTION"},
|
||||
{"restrict delete cascade update", "accounts(id) ON DELETE RESTRICT ON UPDATE CASCADE", false, "public", "accounts", "id", "RESTRICT", "CASCADE"},
|
||||
{"update set null", "accounts(id) ON DELETE NO ACTION ON UPDATE SET NULL", false, "public", "accounts", "id", "NO ACTION", "SET NULL"},
|
||||
{"no parens", "accounts", true, "", "", "", "", ""},
|
||||
{"reversed parens", "accounts)id(", true, "", "", "", "", ""},
|
||||
}
|
||||
r := newTestReader()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
table := models.InitTable("orders", "public")
|
||||
col := models.InitColumn("account_id", "orders", "public")
|
||||
r.createInlineReferenceConstraint(table, col, tt.ref)
|
||||
if tt.wantNone {
|
||||
if len(table.Constraints) != 0 {
|
||||
t.Fatalf("unexpected constraints: %v", table.Constraints)
|
||||
}
|
||||
return
|
||||
}
|
||||
c := table.Constraints["fk_orders_account_id"]
|
||||
if c == nil {
|
||||
t.Fatal("constraint missing")
|
||||
}
|
||||
if c.ReferencedSchema != tt.schema || c.ReferencedTable != tt.table ||
|
||||
c.ReferencedColumns[0] != tt.col || c.OnDelete != tt.onDelete || c.OnUpdate != tt.onUpdate {
|
||||
t.Errorf("constraint = %+v", c)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package pgsql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
)
|
||||
|
||||
func TestNormalizePostgresDefault(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"empty", "", ""},
|
||||
{"function", "now()", "now()"},
|
||||
{"nextval passthrough", "nextval('seq'::regclass)", "nextval('seq'::regclass)"},
|
||||
{"number", "42", "42"},
|
||||
{"null cast", "NULL::text", "NULL::text"},
|
||||
{"quoted literal", "'abc'", "abc"},
|
||||
{"quoted with cast", "'abc'::character varying", "abc"},
|
||||
{"escaped quote", "'it''s'::text", "it's"},
|
||||
{"empty literal", "''::text", ""},
|
||||
{"only escaped quotes", "''''", "'"},
|
||||
{"unterminated", "'abc", "abc"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := normalizePostgresDefault(tt.in); got != tt.want {
|
||||
t.Errorf("normalizePostgresDefault(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountHelpers(t *testing.T) {
|
||||
cols := map[string]map[string]*models.Column{
|
||||
"a": {"x": {}, "y": {}},
|
||||
"b": {"z": {}},
|
||||
"c": {},
|
||||
}
|
||||
if got := countColumns(cols); got != 3 {
|
||||
t.Errorf("countColumns = %d, want 3", got)
|
||||
}
|
||||
if got := countColumns(nil); got != 0 {
|
||||
t.Errorf("countColumns(nil) = %d, want 0", got)
|
||||
}
|
||||
|
||||
cons := map[string][]*models.Constraint{"a": {{}, {}}, "b": {{}}}
|
||||
if got := countConstraints(cons); got != 3 {
|
||||
t.Errorf("countConstraints = %d, want 3", got)
|
||||
}
|
||||
if got := countConstraints(nil); got != 0 {
|
||||
t.Errorf("countConstraints(nil) = %d, want 0", got)
|
||||
}
|
||||
|
||||
idx := map[string][]*models.Index{"a": {{}}, "b": {{}, {}, {}}}
|
||||
if got := countIndexes(idx); got != 4 {
|
||||
t.Errorf("countIndexes = %d, want 4", got)
|
||||
}
|
||||
if got := countIndexes(nil); got != 0 {
|
||||
t.Errorf("countIndexes(nil) = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractIndexOperatorClass(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in []string
|
||||
want string
|
||||
}{
|
||||
{"none", nil, ""},
|
||||
{"sort modifiers only", []string{"DESC", "NULLS", "LAST"}, ""},
|
||||
{"opclass", []string{"", " Vector_Cosine_Ops "}, "vector_cosine_ops"},
|
||||
{"opclass after ordering", []string{"desc", "gin_trgm_ops"}, "gin_trgm_ops"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := extractIndexOperatorClass(tt.in); got != tt.want {
|
||||
t.Errorf("got %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildIndexHint(t *testing.T) {
|
||||
tests := []struct {
|
||||
opClass, params, want string
|
||||
}{
|
||||
{"", "", ""},
|
||||
{"vector_cosine_ops", "", "opclass=vector_cosine_ops"},
|
||||
{"", "m=16", "with (m=16)"},
|
||||
{"vector_cosine_ops", "m=16", "opclass=vector_cosine_ops; with (m=16)"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := buildIndexHint(tt.opClass, tt.params); got != tt.want {
|
||||
t.Errorf("buildIndexHint(%q,%q) = %q, want %q", tt.opClass, tt.params, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeIndexStorageParams(t *testing.T) {
|
||||
tests := []struct{ in, want string }{
|
||||
{"", ""},
|
||||
{"m='16', ef_construction='64'", "m=16, ef_construction=64"},
|
||||
{"key_field='id'", "key_field='id'"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := normalizeIndexStorageParams(tt.in); got != tt.want {
|
||||
t.Errorf("normalizeIndexStorageParams(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package prisma
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
)
|
||||
|
||||
const examplePrisma = "../../../tests/assets/prisma/example.prisma"
|
||||
|
||||
func readFixture(t *testing.T) *models.Schema {
|
||||
t.Helper()
|
||||
db, err := NewReader(&readers.ReaderOptions{FilePath: examplePrisma}).ReadDatabase()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db.Schemas[0]
|
||||
}
|
||||
|
||||
func readSource(t *testing.T, src string) *models.Database {
|
||||
t.Helper()
|
||||
p := filepath.Join(t.TempDir(), "schema.prisma")
|
||||
if err := os.WriteFile(p, []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := NewReader(&readers.ReaderOptions{FilePath: p}).ReadDatabase()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func table(s *models.Schema, name string) *models.Table {
|
||||
for _, tb := range s.Tables {
|
||||
if tb.Name == name {
|
||||
return tb
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestFixture_NoRelationFieldColumns(t *testing.T) {
|
||||
s := readFixture(t)
|
||||
// Relation fields (user, author, posts, profile, categories) are not columns.
|
||||
for tbl, fields := range map[string][]string{
|
||||
"User": {"posts", "profile"}, "Profile": {"user"}, "Post": {"author", "categories"}, "Category": {"posts"},
|
||||
} {
|
||||
for _, f := range fields {
|
||||
if _, ok := table(s, tbl).Columns[f]; ok {
|
||||
t.Errorf("%s.%s is a relation field and must not be a column", tbl, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Enum-typed fields stay columns.
|
||||
if c := table(s, "User").Columns["role"]; c == nil || c.Type != "Role" || c.Default != "USER" {
|
||||
t.Errorf("User.role: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixture_Structure(t *testing.T) {
|
||||
s := readFixture(t)
|
||||
if len(s.Enums) != 1 || s.Enums[0].Name != "Role" || len(s.Enums[0].Values) != 2 {
|
||||
t.Errorf("enums: %+v", s.Enums)
|
||||
}
|
||||
for _, n := range []string{"User", "Profile", "Post", "Category", "_CategoryToPost"} {
|
||||
if table(s, n) == nil {
|
||||
t.Errorf("table %s missing", n)
|
||||
}
|
||||
}
|
||||
|
||||
user := table(s, "User")
|
||||
if id := user.Columns["id"]; id == nil || !id.IsPrimaryKey || !id.AutoIncrement || id.Type != "integer" {
|
||||
t.Errorf("User.id: %+v", id)
|
||||
}
|
||||
if c := user.Columns["name"]; c == nil || c.NotNull {
|
||||
t.Errorf("optional name: %+v", c)
|
||||
}
|
||||
if uq := user.Constraints["uq_email"]; uq == nil || uq.Columns[0] != "email" {
|
||||
t.Errorf("unique: %+v", user.Constraints)
|
||||
}
|
||||
|
||||
post := table(s, "Post")
|
||||
if c := post.Columns["createdAt"]; c == nil || c.Type != "timestamp" || c.Default != "now()" {
|
||||
t.Errorf("createdAt: %+v", c)
|
||||
}
|
||||
if c := post.Columns["updatedAt"]; c == nil || !strings.Contains(c.Comment, "@updatedAt") {
|
||||
t.Errorf("updatedAt: %+v", c)
|
||||
}
|
||||
if c := post.Columns["published"]; c == nil || c.Default != false {
|
||||
t.Errorf("published default: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixture_Relations(t *testing.T) {
|
||||
s := readFixture(t)
|
||||
|
||||
fk := table(s, "Post").Constraints["fk_Post_authorId"]
|
||||
if fk == nil || fk.Type != models.ForeignKeyConstraint || fk.Columns[0] != "authorId" || fk.ReferencedTable != "User" || fk.ReferencedColumns[0] != "id" {
|
||||
t.Errorf("Post.author fk: %+v", fk)
|
||||
}
|
||||
|
||||
jt := table(s, "_CategoryToPost")
|
||||
if len(jt.Columns) != 2 {
|
||||
t.Fatalf("join columns: %v", jt.Columns)
|
||||
}
|
||||
var pk, fks int
|
||||
for _, c := range jt.Constraints {
|
||||
switch c.Type {
|
||||
case models.PrimaryKeyConstraint:
|
||||
pk++
|
||||
case models.ForeignKeyConstraint:
|
||||
fks++
|
||||
if c.OnDelete != "Cascade" {
|
||||
t.Errorf("join fk on delete: %q", c.OnDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
if pk != 1 || fks != 2 {
|
||||
t.Errorf("join constraints: pk=%d fks=%d", pk, fks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlockAttributesAndDefaults(t *testing.T) {
|
||||
db := readSource(t, `datasource db {
|
||||
provider = "mysql"
|
||||
}
|
||||
|
||||
model Membership {
|
||||
userId Int
|
||||
groupId Int
|
||||
role String @default("member")
|
||||
alias String @default('x')
|
||||
score Float @default(1.5)
|
||||
tag String @default(cuid())
|
||||
token String @default(uuid())
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Restrict)
|
||||
@@id([userId, groupId])
|
||||
@@unique([userId, role])
|
||||
@@index([groupId])
|
||||
@@map("memberships")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id
|
||||
memberships Membership[]
|
||||
slug String @unique @default(dbgenerated("abc(1)"))
|
||||
}
|
||||
`)
|
||||
if db.DatabaseType != "mysql" {
|
||||
t.Errorf("db type: %q", db.DatabaseType)
|
||||
}
|
||||
m := table(db.Schemas[0], "Membership")
|
||||
|
||||
pk := m.Constraints["pk_Membership"]
|
||||
if pk == nil || len(pk.Columns) != 2 || !m.Columns["userId"].IsPrimaryKey || !m.Columns["groupId"].NotNull {
|
||||
t.Errorf("composite pk: %+v", pk)
|
||||
}
|
||||
if uq := m.Constraints["uq_Membership_userId_role"]; uq == nil || len(uq.Columns) != 2 {
|
||||
t.Errorf("composite unique: %+v", m.Constraints)
|
||||
}
|
||||
if ix := m.Indexes["idx_Membership_groupId"]; ix == nil || ix.Columns[0] != "groupId" {
|
||||
t.Errorf("index: %+v", m.Indexes)
|
||||
}
|
||||
|
||||
checks := map[string]any{"role": "member", "alias": "x", "score": "1.5"}
|
||||
for col, want := range checks {
|
||||
if got := m.Columns[col].Default; got != want {
|
||||
t.Errorf("%s default = %#v, want %#v", col, got, want)
|
||||
}
|
||||
}
|
||||
if m.Columns["tag"].Comment != "default(cuid())" {
|
||||
t.Errorf("cuid comment: %q", m.Columns["tag"].Comment)
|
||||
}
|
||||
if m.Columns["token"].Default != "gen_random_uuid()" {
|
||||
t.Errorf("uuid default: %v", m.Columns["token"].Default)
|
||||
}
|
||||
if m.Columns["score"].Type != "double precision" {
|
||||
t.Errorf("score type: %s", m.Columns["score"].Type)
|
||||
}
|
||||
|
||||
fk := m.Constraints["fk_Membership_userId"]
|
||||
if fk == nil || fk.OnDelete != "Cascade" || fk.OnUpdate != "Restrict" {
|
||||
t.Errorf("fk actions: %+v", fk)
|
||||
}
|
||||
|
||||
// Default with nested parentheses is extracted whole.
|
||||
if got := table(db.Schemas[0], "User").Columns["slug"].Default; got != `dbgenerated("abc(1)")` {
|
||||
t.Errorf("nested default: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumDeclaredAfterModel(t *testing.T) {
|
||||
db := readSource(t, `model Account {
|
||||
id Int @id
|
||||
status Status @default(ACTIVE)
|
||||
owner Owner?
|
||||
}
|
||||
|
||||
model Owner {
|
||||
id Int @id
|
||||
}
|
||||
|
||||
enum Status {
|
||||
ACTIVE
|
||||
CLOSED
|
||||
}
|
||||
`)
|
||||
a := table(db.Schemas[0], "Account")
|
||||
if c := a.Columns["status"]; c == nil || c.Type != "Status" {
|
||||
t.Errorf("enum column declared before enum: %+v", c)
|
||||
}
|
||||
if _, ok := a.Columns["owner"]; ok {
|
||||
t.Error("model-typed field must not be a column")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDatasourceProviders(t *testing.T) {
|
||||
r := &Reader{}
|
||||
tests := []struct {
|
||||
provider string
|
||||
want models.DatabaseType
|
||||
}{
|
||||
{`"postgresql"`, models.PostgresqlDatabaseType}, {`"postgres"`, models.PostgresqlDatabaseType},
|
||||
{`"mysql"`, "mysql"}, {`"sqlite"`, models.SqlLiteDatabaseType},
|
||||
{`"sqlserver"`, models.MSSQLDatabaseType}, {`"cockroachdb"`, models.PostgresqlDatabaseType},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
db := models.InitDatabase("d")
|
||||
r.parseDatasource([]string{" provider = " + tt.provider}, db)
|
||||
if db.DatabaseType != tt.want {
|
||||
t.Errorf("%s -> %q, want %q", tt.provider, db.DatabaseType, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGenerator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
lines []string
|
||||
opts *readers.ReaderOptions
|
||||
want string
|
||||
}{
|
||||
{"js client", []string{`provider = "prisma-client-js"`}, &readers.ReaderOptions{}, "prisma"},
|
||||
{"new client", []string{`provider = "prisma-client"`}, &readers.ReaderOptions{}, "prisma7"},
|
||||
{"no provider, flag", []string{`output = "x"`}, &readers.ReaderOptions{Prisma7: true}, "prisma7"},
|
||||
{"no provider, nil options", nil, nil, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db := models.InitDatabase("d")
|
||||
db.SourceFormat = ""
|
||||
(&Reader{options: tt.opts}).parseGenerator(tt.lines, db)
|
||||
if db.SourceFormat != tt.want {
|
||||
t.Errorf("got %q, want %q", db.SourceFormat, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrisma7FlagWithoutGeneratorBlock(t *testing.T) {
|
||||
p := filepath.Join(t.TempDir(), "s.prisma")
|
||||
if err := os.WriteFile(p, []byte("model A {\n id Int @id\n}\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := NewReader(&readers.ReaderOptions{FilePath: p, Prisma7: true}).ReadDatabase()
|
||||
if err != nil || db.SourceFormat != "prisma7" {
|
||||
t.Errorf("%v %q", err, db.SourceFormat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetadataNameAndComments(t *testing.T) {
|
||||
p := filepath.Join(t.TempDir(), "s.prisma")
|
||||
src := "// leading comment\nmodel A {\n // inner comment\n id Int @id\n}\n"
|
||||
if err := os.WriteFile(p, []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := NewReader(&readers.ReaderOptions{FilePath: p, Metadata: map[string]any{"name": "shop"}}).ReadDatabase()
|
||||
if err != nil || db.Name != "shop" || len(db.Schemas[0].Tables[0].Columns) != 1 {
|
||||
t.Errorf("%v %+v", err, db)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadSchemaAndTable(t *testing.T) {
|
||||
r := NewReader(&readers.ReaderOptions{FilePath: examplePrisma})
|
||||
s, err := r.ReadSchema()
|
||||
if err != nil || s.Name != "public" {
|
||||
t.Fatalf("schema: %v", err)
|
||||
}
|
||||
tbl, err := r.ReadTable()
|
||||
if err != nil || tbl.Name != "User" {
|
||||
t.Fatalf("table: %v %+v", err, tbl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReader_Errors(t *testing.T) {
|
||||
if _, err := NewReader(&readers.ReaderOptions{}).ReadDatabase(); err == nil || !strings.Contains(err.Error(), "file path is required") {
|
||||
t.Errorf("empty path: %v", err)
|
||||
}
|
||||
if _, err := NewReader(&readers.ReaderOptions{FilePath: filepath.Join(t.TempDir(), "x")}).ReadDatabase(); err == nil || !strings.Contains(err.Error(), "failed to read file") {
|
||||
t.Errorf("missing file: %v", err)
|
||||
}
|
||||
if _, err := NewReader(&readers.ReaderOptions{}).ReadSchema(); err == nil {
|
||||
t.Error("ReadSchema without path")
|
||||
}
|
||||
if _, err := NewReader(&readers.ReaderOptions{}).ReadTable(); err == nil {
|
||||
t.Error("ReadTable without path")
|
||||
}
|
||||
empty := filepath.Join(t.TempDir(), "e.prisma")
|
||||
if err := os.WriteFile(empty, []byte("// nothing\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := NewReader(&readers.ReaderOptions{FilePath: empty}).ReadTable(); err == nil || !strings.Contains(err.Error(), "no tables found") {
|
||||
t.Errorf("ReadTable on empty: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractDefaultValue(t *testing.T) {
|
||||
r := &Reader{}
|
||||
tests := []struct{ in, want string }{
|
||||
{"@id @default(autoincrement())", "autoincrement()"},
|
||||
{`@default("a(b)")`, `"a(b)"`},
|
||||
{"@unique", ""},
|
||||
{"@default(unclosed(", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := r.extractDefaultValue(tt.in); got != tt.want {
|
||||
t.Errorf("%q = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrismaTypeToSQL(t *testing.T) {
|
||||
r := &Reader{}
|
||||
tests := map[string]string{
|
||||
"String": "text", "Boolean": "boolean", "Int": "integer", "BigInt": "bigint",
|
||||
"Float": "double precision", "Decimal": "decimal", "DateTime": "timestamp",
|
||||
"Json": "jsonb", "Bytes": "bytea", "Custom": "Custom",
|
||||
}
|
||||
for in, want := range tests {
|
||||
if got := r.prismaTypeToSQL(in); got != want {
|
||||
t.Errorf("%s = %s, want %s", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
package typeorm
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/readers"
|
||||
)
|
||||
|
||||
const exampleTS = "../../../tests/assets/typeorm/example.ts"
|
||||
|
||||
func readFixture(t *testing.T) *models.Schema {
|
||||
t.Helper()
|
||||
db, err := NewReader(&readers.ReaderOptions{FilePath: exampleTS}).ReadDatabase()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(db.Schemas) != 1 {
|
||||
t.Fatalf("schemas: %d", len(db.Schemas))
|
||||
}
|
||||
return db.Schemas[0]
|
||||
}
|
||||
|
||||
func tableByName(s *models.Schema, name string) *models.Table {
|
||||
for _, t := range s.Tables {
|
||||
if t.Name == name {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseSource(t *testing.T, src string) *models.Schema {
|
||||
t.Helper()
|
||||
db, err := NewReader(&readers.ReaderOptions{}).parseTypeORM(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db.Schemas[0]
|
||||
}
|
||||
|
||||
func TestReadFixture_Tables(t *testing.T) {
|
||||
s := readFixture(t)
|
||||
for _, name := range []string{"User", "Project", "Task", "Comment", "Tag", "user_project", "tag_task"} {
|
||||
if tableByName(s, name) == nil {
|
||||
t.Errorf("table %q missing", name)
|
||||
}
|
||||
}
|
||||
if len(s.Tables) != 7 {
|
||||
t.Errorf("tables: %d, want 7 (5 entities + 2 join tables)", len(s.Tables))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFixture_ColumnsAndKeys(t *testing.T) {
|
||||
s := readFixture(t)
|
||||
|
||||
user := tableByName(s, "User")
|
||||
id := user.Columns["id"]
|
||||
if id == nil || id.Type != "uuid" || !id.IsPrimaryKey || id.Default != "gen_random_uuid()" {
|
||||
t.Errorf("User.id: %+v", id)
|
||||
}
|
||||
if c := user.Columns["createdAt"]; c == nil || c.Type != "timestamp" || c.Default != "now()" {
|
||||
t.Errorf("User.createdAt: %+v", c)
|
||||
}
|
||||
if c := user.Columns["updatedAt"]; c == nil || c.Type != "timestamp" || !strings.Contains(c.Comment, "auto-update") {
|
||||
t.Errorf("User.updatedAt: %+v", c)
|
||||
}
|
||||
if uq := user.Constraints["uq_email"]; uq == nil || uq.Type != models.UniqueConstraint || uq.Columns[0] != "email" {
|
||||
t.Errorf("unique email: %+v", user.Constraints)
|
||||
}
|
||||
if _, ok := user.Columns["ownedProjects"]; ok {
|
||||
t.Error("relation fields must not become columns")
|
||||
}
|
||||
|
||||
project := tableByName(s, "Project")
|
||||
if c := project.Columns["description"]; c == nil || c.NotNull {
|
||||
t.Errorf("nullable description: %+v", c)
|
||||
}
|
||||
if c := project.Columns["status"]; c == nil || c.Default != "active" {
|
||||
t.Errorf("status default: %+v", c)
|
||||
}
|
||||
if c := tableByName(s, "Task").Columns["description"]; c == nil || c.Type != "text" || c.NotNull {
|
||||
t.Errorf("Task.description: %+v", c)
|
||||
}
|
||||
if c := tableByName(s, "Comment").Columns["content"]; c == nil || c.Type != "text" {
|
||||
t.Errorf("shorthand type: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFixture_Relationships(t *testing.T) {
|
||||
s := readFixture(t)
|
||||
|
||||
fk := tableByName(s, "Project").Constraints["fk_Project_owner"]
|
||||
if fk == nil || fk.Type != models.ForeignKeyConstraint || fk.Columns[0] != "ownerId" || fk.ReferencedTable != "User" {
|
||||
t.Errorf("Project.owner fk: %+v", fk)
|
||||
}
|
||||
if c := tableByName(s, "Project").Columns["ownerId"]; c == nil || c.Type != "uuid" || !c.NotNull {
|
||||
t.Errorf("ownerId column: %+v", c)
|
||||
}
|
||||
// ManyToOne with { nullable: true } produces a nullable FK column.
|
||||
if c := tableByName(s, "Task").Columns["assigneeId"]; c == nil || c.NotNull {
|
||||
t.Errorf("assigneeId must be nullable: %+v", c)
|
||||
}
|
||||
|
||||
for _, jt := range []string{"user_project", "tag_task"} {
|
||||
tbl := tableByName(s, jt)
|
||||
if len(tbl.Columns) != 2 {
|
||||
t.Errorf("%s columns: %d", jt, len(tbl.Columns))
|
||||
}
|
||||
pk := 0
|
||||
fks := 0
|
||||
for _, c := range tbl.Constraints {
|
||||
switch c.Type {
|
||||
case models.PrimaryKeyConstraint:
|
||||
pk++
|
||||
if len(c.Columns) != 2 {
|
||||
t.Errorf("%s composite pk: %v", jt, c.Columns)
|
||||
}
|
||||
case models.ForeignKeyConstraint:
|
||||
fks++
|
||||
}
|
||||
}
|
||||
if pk != 1 || fks != 2 {
|
||||
t.Errorf("%s: pk=%d fks=%d", jt, pk, fks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadSchemaAndTable(t *testing.T) {
|
||||
r := NewReader(&readers.ReaderOptions{FilePath: exampleTS})
|
||||
s, err := r.ReadSchema()
|
||||
if err != nil || s.Name != "public" {
|
||||
t.Fatalf("schema: %v %+v", err, s)
|
||||
}
|
||||
tbl, err := r.ReadTable()
|
||||
if err != nil || tbl.Name != "User" {
|
||||
t.Fatalf("table: %v %+v", err, tbl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReader_Errors(t *testing.T) {
|
||||
if _, err := NewReader(&readers.ReaderOptions{}).ReadDatabase(); err == nil || !strings.Contains(err.Error(), "file path is required") {
|
||||
t.Errorf("empty path: %v", err)
|
||||
}
|
||||
if _, err := NewReader(&readers.ReaderOptions{FilePath: filepath.Join(t.TempDir(), "x.ts")}).ReadDatabase(); err == nil || !strings.Contains(err.Error(), "failed to read file") {
|
||||
t.Errorf("missing file: %v", err)
|
||||
}
|
||||
if _, err := NewReader(&readers.ReaderOptions{}).ReadSchema(); err == nil {
|
||||
t.Error("ReadSchema without path must fail")
|
||||
}
|
||||
if _, err := NewReader(&readers.ReaderOptions{}).ReadTable(); err == nil {
|
||||
t.Error("ReadTable without path must fail")
|
||||
}
|
||||
|
||||
empty := filepath.Join(t.TempDir(), "empty.ts")
|
||||
if err := os.WriteFile(empty, []byte("// nothing here\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := NewReader(&readers.ReaderOptions{FilePath: empty})
|
||||
if db, err := r.ReadDatabase(); err != nil || len(db.Schemas[0].Tables) != 0 {
|
||||
t.Errorf("empty file: %v %+v", err, db)
|
||||
}
|
||||
if _, err := r.ReadTable(); err == nil || !strings.Contains(err.Error(), "no tables found") {
|
||||
t.Errorf("ReadTable on empty: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityOptions(t *testing.T) {
|
||||
s := parseSource(t, `
|
||||
@Entity({ name: "app_users", schema: "auth", database: "main", engine: "InnoDB" })
|
||||
export class User {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, nullable: true })
|
||||
login: string;
|
||||
|
||||
@Column({ type: 'numeric', precision: 12, scale: 4 })
|
||||
balance: number;
|
||||
|
||||
@Column({ type: 'boolean' })
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
@Entity('legacy')
|
||||
export class Legacy {
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id: number;
|
||||
|
||||
@Column('jsonb')
|
||||
payload: any;
|
||||
}
|
||||
`)
|
||||
user := tableByName(s, "app_users")
|
||||
if user == nil || user.Schema != "auth" {
|
||||
t.Fatalf("tables: %+v", s.Tables)
|
||||
}
|
||||
if c := user.Columns["id"]; c == nil || !c.AutoIncrement || c.Type != "integer" {
|
||||
t.Errorf("id: %+v", c)
|
||||
}
|
||||
if c := user.Columns["login"]; c == nil || c.Type != "varchar(100)" || c.Length != 100 || c.NotNull {
|
||||
t.Errorf("login: %+v", c)
|
||||
}
|
||||
if c := user.Columns["balance"]; c == nil || c.Type != "numeric(12,4)" {
|
||||
t.Errorf("balance: %+v", c)
|
||||
}
|
||||
if c := user.Columns["active"]; c == nil || c.Type != "boolean" {
|
||||
t.Errorf("active: %+v", c)
|
||||
}
|
||||
if c := tableByName(s, "Legacy").Columns["payload"]; c == nil || c.Type != "jsonb" {
|
||||
t.Errorf("payload: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewEntity(t *testing.T) {
|
||||
s := parseSource(t, `
|
||||
@ViewEntity({
|
||||
name: "active_users",
|
||||
schema: "reporting",
|
||||
expression: `+"`"+`SELECT id, email FROM users WHERE active`+"`"+`
|
||||
})
|
||||
export class ActiveUsers {
|
||||
id: number;
|
||||
email: string;
|
||||
}
|
||||
|
||||
@ViewEntity({ expression: "SELECT 1" })
|
||||
export class OneView {
|
||||
n: number;
|
||||
}
|
||||
`)
|
||||
if len(s.Views) != 2 || len(s.Tables) != 0 {
|
||||
t.Fatalf("views=%d tables=%d", len(s.Views), len(s.Tables))
|
||||
}
|
||||
v := s.Views[0]
|
||||
if v.Name != "active_users" || v.Schema != "reporting" || !strings.Contains(v.Definition, "SELECT id, email FROM users") {
|
||||
t.Errorf("view: %+v", v)
|
||||
}
|
||||
if c := v.Columns["email"]; c == nil || c.Type != "text" {
|
||||
t.Errorf("view column: %+v", v.Columns)
|
||||
}
|
||||
if s.Views[1].Name != "OneView" || s.Views[1].Definition != "SELECT 1" {
|
||||
t.Errorf("second view: %+v", s.Views[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseColumnDecorator_IdentityAndGenerated(t *testing.T) {
|
||||
r := &Reader{}
|
||||
tbl := models.InitTable("t", "public")
|
||||
|
||||
col := models.InitColumn("id", "t", "public")
|
||||
r.parseColumnDecorator(`@PrimaryGeneratedColumn('identity', { generatedIdentity: 'ALWAYS' })`, col, tbl)
|
||||
if !col.IsPrimaryKey || !col.Identity || !col.AutoIncrement {
|
||||
t.Errorf("identity pk: %+v", col)
|
||||
}
|
||||
|
||||
other := models.InitColumn("seq", "t", "public")
|
||||
r.parseColumnDecorator(`@Generated('identity')`, other, tbl)
|
||||
if !other.Identity || other.IdentityGeneration != "BY DEFAULT" {
|
||||
t.Errorf("@Generated: %+v", other)
|
||||
}
|
||||
r.parseColumnDecorator(`@Generated('uuid')`, models.InitColumn("u", "t", "public"), tbl) // no-op, no panic
|
||||
|
||||
gen := models.InitColumn("full", "t", "public")
|
||||
r.parseColumnOptions(`@Column({ type: 'text', generatedType: 'STORED', asExpression: 'a || \'x\'' })`, gen, tbl)
|
||||
if !gen.Generated || !strings.Contains(gen.GenerationExpression, "a ||") {
|
||||
t.Errorf("generated column: %+v", gen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGeneratedIdentity(t *testing.T) {
|
||||
tests := []struct{ in, want string }{
|
||||
{`{ generatedIdentity: 'ALWAYS' }`, "ALWAYS"},
|
||||
{`{ generatedIdentity: 'BY DEFAULT' }`, "BY DEFAULT"},
|
||||
{`no option`, "BY DEFAULT"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := parseGeneratedIdentity(tt.in); got != tt.want {
|
||||
t.Errorf("%q = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnescapeSingleQuoted(t *testing.T) {
|
||||
tests := []struct{ in, want string }{
|
||||
{"plain", "plain"}, {`it\'s`, "it's"}, {`a\\b`, `a\b`}, {`trailing\`, `trailing\`}, {"", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := unescapeSingleQuoted(tt.in); got != tt.want {
|
||||
t.Errorf("unescape(%q) = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchDecorator(t *testing.T) {
|
||||
tests := []struct {
|
||||
line string
|
||||
want string
|
||||
wantOK bool
|
||||
}{
|
||||
{"@Entity()", "@Entity()", true},
|
||||
{"@Column() name: string;", "@Column()", true},
|
||||
{"@Column({ type: 'text' })", "@Column({ type: 'text' })", true},
|
||||
{`@Column({ asExpression: 'f(a)' }) x: string;`, `@Column({ asExpression: 'f(a)' })`, true},
|
||||
{"@Generated", "@Generated", true},
|
||||
{"@Column({ unterminated", "@Column({ unterminated", true},
|
||||
{"name: string;", "", false},
|
||||
{"", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, ok := matchDecorator(tt.line)
|
||||
if got != tt.want || ok != tt.wantOK {
|
||||
t.Errorf("matchDecorator(%q) = (%q,%v), want (%q,%v)", tt.line, got, ok, tt.want, tt.wantOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypeScriptTypeToSQL(t *testing.T) {
|
||||
r := &Reader{}
|
||||
tests := []struct{ in, want string }{
|
||||
{"string", "text"}, {"number", "integer"}, {"boolean", "boolean"}, {"Date", "timestamp"},
|
||||
{"any", "jsonb"}, {"string[]", "text"}, {"string | null", "text"}, {"Unknown", "text"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := r.typeScriptTypeToSQL(tt.in); got != tt.want {
|
||||
t.Errorf("%q = %q, want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRelationField(t *testing.T) {
|
||||
r := &Reader{}
|
||||
for _, d := range []string{"@ManyToOne(() => A)", "@OneToMany(() => A, a => a.b)", "@ManyToMany(() => A)", "@OneToOne(() => A)"} {
|
||||
if !r.isRelationField(fieldInfo{decorators: []string{d}}) {
|
||||
t.Errorf("%s should be a relation", d)
|
||||
}
|
||||
}
|
||||
if r.isRelationField(fieldInfo{decorators: []string{"@Column()"}}) || r.isRelationField(fieldInfo{}) {
|
||||
t.Error("non-relation misdetected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOneToOne_And_MultiLineDecorators(t *testing.T) {
|
||||
s := parseSource(t, `
|
||||
@Entity()
|
||||
export class Profile {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
nullable: true,
|
||||
})
|
||||
bio: string;
|
||||
|
||||
@OneToOne(() => Account)
|
||||
@JoinColumn()
|
||||
account: Account;
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Account {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
}
|
||||
`)
|
||||
p := tableByName(s, "Profile")
|
||||
if c := p.Columns["bio"]; c == nil || c.Type != "varchar(50)" || c.NotNull {
|
||||
t.Errorf("multi-line @Column not parsed: %+v", c)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user