mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-11-13 18:03:53 +00:00
Fixed tablename and schema lookups
This commit is contained in:
parent
e88018543e
commit
3b2d05465e
138
SCHEMA_TABLE_HANDLING.md
Normal file
138
SCHEMA_TABLE_HANDLING.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# Schema and Table Name Handling
|
||||||
|
|
||||||
|
This document explains how the handlers properly separate and handle schema and table names.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Both `resolvespec` and `restheadspec` handlers now properly handle schema and table name separation through the following functions:
|
||||||
|
|
||||||
|
- `parseTableName(fullTableName)` - Splits "schema.table" into separate components
|
||||||
|
- `getSchemaAndTable(defaultSchema, entity, model)` - Returns schema and table separately
|
||||||
|
- `getTableName(schema, entity, model)` - Returns the full "schema.table" format
|
||||||
|
|
||||||
|
## Priority Order
|
||||||
|
|
||||||
|
When determining the schema and table name, the following priority is used:
|
||||||
|
|
||||||
|
1. **If `TableName()` contains a schema** (e.g., "myschema.mytable"), that schema takes precedence
|
||||||
|
2. **If model implements `SchemaProvider`**, use that schema
|
||||||
|
3. **Otherwise**, use the `defaultSchema` parameter from the URL/request
|
||||||
|
|
||||||
|
## Scenarios
|
||||||
|
|
||||||
|
### Scenario 1: Simple table name, default schema
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Request URL: `/api/public/users`
|
||||||
|
- Result: `schema="public"`, `table="users"`, `fullName="public.users"`
|
||||||
|
|
||||||
|
### Scenario 2: Table name includes schema
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "auth.users" // Schema included!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Request URL: `/api/public/users` (public is ignored)
|
||||||
|
- Result: `schema="auth"`, `table="users"`, `fullName="auth.users"`
|
||||||
|
- **Note**: The schema from `TableName()` takes precedence over the URL schema
|
||||||
|
|
||||||
|
### Scenario 3: Using SchemaProvider
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (User) SchemaName() string {
|
||||||
|
return "auth"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Request URL: `/api/public/users` (public is ignored)
|
||||||
|
- Result: `schema="auth"`, `table="users"`, `fullName="auth.users"`
|
||||||
|
|
||||||
|
### Scenario 4: Table name includes schema AND SchemaProvider
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "core.users" // This wins!
|
||||||
|
}
|
||||||
|
|
||||||
|
func (User) SchemaName() string {
|
||||||
|
return "auth" // This is ignored
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Request URL: `/api/public/users`
|
||||||
|
- Result: `schema="core"`, `table="users"`, `fullName="core.users"`
|
||||||
|
- **Note**: Schema from `TableName()` takes highest precedence
|
||||||
|
|
||||||
|
### Scenario 5: No providers at all
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
// No TableName() or SchemaName()
|
||||||
|
```
|
||||||
|
- Request URL: `/api/public/users`
|
||||||
|
- Result: `schema="public"`, `table="users"`, `fullName="public.users"`
|
||||||
|
- Uses URL schema and entity name
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
1. **Automatic detection**: The code automatically detects if `TableName()` includes a schema by checking for "."
|
||||||
|
2. **Backward compatible**: Existing code continues to work
|
||||||
|
3. **Flexible**: Supports multiple ways to specify schema and table
|
||||||
|
4. **Debug logging**: Logs when schema is detected in `TableName()` for debugging
|
||||||
|
|
||||||
|
## Code Locations
|
||||||
|
|
||||||
|
### Handlers
|
||||||
|
- `/pkg/resolvespec/handler.go:472-531`
|
||||||
|
- `/pkg/restheadspec/handler.go:534-593`
|
||||||
|
|
||||||
|
### Database Adapters
|
||||||
|
- `/pkg/common/adapters/database/utils.go` - Shared `parseTableName()` function
|
||||||
|
- `/pkg/common/adapters/database/bun.go` - Bun adapter with separated schema/table
|
||||||
|
- `/pkg/common/adapters/database/gorm.go` - GORM adapter with separated schema/table
|
||||||
|
|
||||||
|
## Adapter Implementation
|
||||||
|
|
||||||
|
Both Bun and GORM adapters now properly separate schema and table name:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// BunSelectQuery/GormSelectQuery now have separated fields:
|
||||||
|
type BunSelectQuery struct {
|
||||||
|
query *bun.SelectQuery
|
||||||
|
schema string // Separated schema name
|
||||||
|
tableName string // Just the table name, without schema
|
||||||
|
tableAlias string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When `Model()` or `Table()` is called:
|
||||||
|
1. The full table name (which may include schema) is parsed
|
||||||
|
2. Schema and table name are stored separately
|
||||||
|
3. When building joins, the already-separated table name is used directly
|
||||||
|
|
||||||
|
This ensures consistent handling of schema-qualified table names throughout the codebase.
|
||||||
@ -78,7 +78,8 @@ func (b *BunAdapter) RunInTransaction(ctx context.Context, fn func(common.Databa
|
|||||||
// BunSelectQuery implements SelectQuery for Bun
|
// BunSelectQuery implements SelectQuery for Bun
|
||||||
type BunSelectQuery struct {
|
type BunSelectQuery struct {
|
||||||
query *bun.SelectQuery
|
query *bun.SelectQuery
|
||||||
tableName string
|
schema string // Separated schema name
|
||||||
|
tableName string // Just the table name, without schema
|
||||||
tableAlias string
|
tableAlias string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +88,9 @@ func (b *BunSelectQuery) Model(model interface{}) common.SelectQuery {
|
|||||||
|
|
||||||
// Try to get table name from model if it implements TableNameProvider
|
// Try to get table name from model if it implements TableNameProvider
|
||||||
if provider, ok := model.(common.TableNameProvider); ok {
|
if provider, ok := model.(common.TableNameProvider); ok {
|
||||||
b.tableName = provider.TableName()
|
fullTableName := provider.TableName()
|
||||||
|
// Check if the table name contains schema (e.g., "schema.table")
|
||||||
|
b.schema, b.tableName = parseTableName(fullTableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b
|
return b
|
||||||
@ -95,7 +98,8 @@ func (b *BunSelectQuery) Model(model interface{}) common.SelectQuery {
|
|||||||
|
|
||||||
func (b *BunSelectQuery) Table(table string) common.SelectQuery {
|
func (b *BunSelectQuery) Table(table string) common.SelectQuery {
|
||||||
b.query = b.query.Table(table)
|
b.query = b.query.Table(table)
|
||||||
b.tableName = table
|
// Check if the table name contains schema (e.g., "schema.table")
|
||||||
|
b.schema, b.tableName = parseTableName(table)
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,13 +132,9 @@ func (b *BunSelectQuery) Join(query string, args ...interface{}) common.SelectQu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no prefix provided, use the table name as prefix
|
// If no prefix provided, use the table name as prefix (already separated from schema)
|
||||||
if prefix == "" && b.tableName != "" {
|
if prefix == "" && b.tableName != "" {
|
||||||
prefix = b.tableName
|
prefix = b.tableName
|
||||||
// Extract just the table name if it has schema
|
|
||||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
|
||||||
prefix = prefix[idx+1:]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If prefix is provided, add it as an alias in the join
|
// If prefix is provided, add it as an alias in the join
|
||||||
@ -169,12 +169,9 @@ func (b *BunSelectQuery) LeftJoin(query string, args ...interface{}) common.Sele
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no prefix provided, use the table name as prefix
|
// If no prefix provided, use the table name as prefix (already separated from schema)
|
||||||
if prefix == "" && b.tableName != "" {
|
if prefix == "" && b.tableName != "" {
|
||||||
prefix = b.tableName
|
prefix = b.tableName
|
||||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
|
||||||
prefix = prefix[idx+1:]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct LEFT JOIN with prefix
|
// Construct LEFT JOIN with prefix
|
||||||
|
|||||||
@ -70,7 +70,8 @@ func (g *GormAdapter) RunInTransaction(ctx context.Context, fn func(common.Datab
|
|||||||
// GormSelectQuery implements SelectQuery for GORM
|
// GormSelectQuery implements SelectQuery for GORM
|
||||||
type GormSelectQuery struct {
|
type GormSelectQuery struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
tableName string
|
schema string // Separated schema name
|
||||||
|
tableName string // Just the table name, without schema
|
||||||
tableAlias string
|
tableAlias string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +80,9 @@ func (g *GormSelectQuery) Model(model interface{}) common.SelectQuery {
|
|||||||
|
|
||||||
// Try to get table name from model if it implements TableNameProvider
|
// Try to get table name from model if it implements TableNameProvider
|
||||||
if provider, ok := model.(common.TableNameProvider); ok {
|
if provider, ok := model.(common.TableNameProvider); ok {
|
||||||
g.tableName = provider.TableName()
|
fullTableName := provider.TableName()
|
||||||
|
// Check if the table name contains schema (e.g., "schema.table")
|
||||||
|
g.schema, g.tableName = parseTableName(fullTableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return g
|
return g
|
||||||
@ -87,7 +90,8 @@ func (g *GormSelectQuery) Model(model interface{}) common.SelectQuery {
|
|||||||
|
|
||||||
func (g *GormSelectQuery) Table(table string) common.SelectQuery {
|
func (g *GormSelectQuery) Table(table string) common.SelectQuery {
|
||||||
g.db = g.db.Table(table)
|
g.db = g.db.Table(table)
|
||||||
g.tableName = table
|
// Check if the table name contains schema (e.g., "schema.table")
|
||||||
|
g.schema, g.tableName = parseTableName(table)
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,13 +124,9 @@ func (g *GormSelectQuery) Join(query string, args ...interface{}) common.SelectQ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no prefix provided, use the table name as prefix
|
// If no prefix provided, use the table name as prefix (already separated from schema)
|
||||||
if prefix == "" && g.tableName != "" {
|
if prefix == "" && g.tableName != "" {
|
||||||
prefix = g.tableName
|
prefix = g.tableName
|
||||||
// Extract just the table name if it has schema
|
|
||||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
|
||||||
prefix = prefix[idx+1:]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If prefix is provided, add it as an alias in the join
|
// If prefix is provided, add it as an alias in the join
|
||||||
@ -161,12 +161,9 @@ func (g *GormSelectQuery) LeftJoin(query string, args ...interface{}) common.Sel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no prefix provided, use the table name as prefix
|
// If no prefix provided, use the table name as prefix (already separated from schema)
|
||||||
if prefix == "" && g.tableName != "" {
|
if prefix == "" && g.tableName != "" {
|
||||||
prefix = g.tableName
|
prefix = g.tableName
|
||||||
if idx := strings.LastIndex(prefix, "."); idx != -1 {
|
|
||||||
prefix = prefix[idx+1:]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct LEFT JOIN with prefix
|
// Construct LEFT JOIN with prefix
|
||||||
|
|||||||
13
pkg/common/adapters/database/utils.go
Normal file
13
pkg/common/adapters/database/utils.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// parseTableName splits a table name that may contain schema into separate schema and table
|
||||||
|
// For example: "public.users" -> ("public", "users")
|
||||||
|
// "users" -> ("", "users")
|
||||||
|
func parseTableName(fullTableName string) (schema, table string) {
|
||||||
|
if idx := strings.LastIndex(fullTableName, "."); idx != -1 {
|
||||||
|
return fullTableName[:idx], fullTableName[idx+1:]
|
||||||
|
}
|
||||||
|
return "", fullTableName
|
||||||
|
}
|
||||||
@ -469,11 +469,65 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
// parseTableName splits a table name that may contain schema into separate schema and table
|
||||||
if provider, ok := model.(common.TableNameProvider); ok {
|
func (h *Handler) parseTableName(fullTableName string) (schema, table string) {
|
||||||
return provider.TableName()
|
if idx := strings.LastIndex(fullTableName, "."); idx != -1 {
|
||||||
|
return fullTableName[:idx], fullTableName[idx+1:]
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s.%s", schema, entity)
|
return "", fullTableName
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSchemaAndTable returns the schema and table name separately
|
||||||
|
// It checks SchemaProvider and TableNameProvider interfaces and handles cases where
|
||||||
|
// the table name may already include the schema (e.g., "public.users")
|
||||||
|
//
|
||||||
|
// Priority order:
|
||||||
|
// 1. If TableName() contains a schema (e.g., "myschema.mytable"), that schema takes precedence
|
||||||
|
// 2. If model implements SchemaProvider, use that schema
|
||||||
|
// 3. Otherwise, use the defaultSchema parameter
|
||||||
|
func (h *Handler) getSchemaAndTable(defaultSchema, entity string, model interface{}) (schema, table string) {
|
||||||
|
// First check if model provides a table name
|
||||||
|
// We check this FIRST because the table name might already contain the schema
|
||||||
|
if tableProvider, ok := model.(common.TableNameProvider); ok {
|
||||||
|
tableName := tableProvider.TableName()
|
||||||
|
|
||||||
|
// IMPORTANT: Check if the table name already contains a schema (e.g., "schema.table")
|
||||||
|
// This is common when models need to specify a different schema than the default
|
||||||
|
if tableSchema, tableOnly := h.parseTableName(tableName); tableSchema != "" {
|
||||||
|
// Table name includes schema - use it and ignore any other schema providers
|
||||||
|
logger.Debug("TableName() includes schema: %s.%s", tableSchema, tableOnly)
|
||||||
|
return tableSchema, tableOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table name is just the table name without schema
|
||||||
|
// Now determine which schema to use
|
||||||
|
if schemaProvider, ok := model.(common.SchemaProvider); ok {
|
||||||
|
schema = schemaProvider.SchemaName()
|
||||||
|
} else {
|
||||||
|
schema = defaultSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema, tableName
|
||||||
|
}
|
||||||
|
|
||||||
|
// No TableNameProvider, so check for schema and use entity as table name
|
||||||
|
if schemaProvider, ok := model.(common.SchemaProvider); ok {
|
||||||
|
schema = schemaProvider.SchemaName()
|
||||||
|
} else {
|
||||||
|
schema = defaultSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to entity name as table
|
||||||
|
return schema, entity
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTableName returns the full table name including schema (schema.table)
|
||||||
|
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
||||||
|
schemaName, tableName := h.getSchemaAndTable(schema, entity, model)
|
||||||
|
if schemaName != "" {
|
||||||
|
return fmt.Sprintf("%s.%s", schemaName, tableName)
|
||||||
|
}
|
||||||
|
return tableName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) generateMetadata(schema, entity string, model interface{}) *common.TableMetadata {
|
func (h *Handler) generateMetadata(schema, entity string, model interface{}) *common.TableMetadata {
|
||||||
|
|||||||
@ -531,20 +531,65 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
// parseTableName splits a table name that may contain schema into separate schema and table
|
||||||
// Check if model implements TableNameProvider
|
func (h *Handler) parseTableName(fullTableName string) (schema, table string) {
|
||||||
if provider, ok := model.(common.TableNameProvider); ok {
|
if idx := strings.LastIndex(fullTableName, "."); idx != -1 {
|
||||||
tableName := provider.TableName()
|
return fullTableName[:idx], fullTableName[idx+1:]
|
||||||
if tableName != "" {
|
|
||||||
return tableName
|
|
||||||
}
|
}
|
||||||
|
return "", fullTableName
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSchemaAndTable returns the schema and table name separately
|
||||||
|
// It checks SchemaProvider and TableNameProvider interfaces and handles cases where
|
||||||
|
// the table name may already include the schema (e.g., "public.users")
|
||||||
|
//
|
||||||
|
// Priority order:
|
||||||
|
// 1. If TableName() contains a schema (e.g., "myschema.mytable"), that schema takes precedence
|
||||||
|
// 2. If model implements SchemaProvider, use that schema
|
||||||
|
// 3. Otherwise, use the defaultSchema parameter
|
||||||
|
func (h *Handler) getSchemaAndTable(defaultSchema, entity string, model interface{}) (schema, table string) {
|
||||||
|
// First check if model provides a table name
|
||||||
|
// We check this FIRST because the table name might already contain the schema
|
||||||
|
if tableProvider, ok := model.(common.TableNameProvider); ok {
|
||||||
|
tableName := tableProvider.TableName()
|
||||||
|
|
||||||
|
// IMPORTANT: Check if the table name already contains a schema (e.g., "schema.table")
|
||||||
|
// This is common when models need to specify a different schema than the default
|
||||||
|
if tableSchema, tableOnly := h.parseTableName(tableName); tableSchema != "" {
|
||||||
|
// Table name includes schema - use it and ignore any other schema providers
|
||||||
|
logger.Debug("TableName() includes schema: %s.%s", tableSchema, tableOnly)
|
||||||
|
return tableSchema, tableOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to schema.entity
|
// Table name is just the table name without schema
|
||||||
if schema != "" {
|
// Now determine which schema to use
|
||||||
return fmt.Sprintf("%s.%s", schema, entity)
|
if schemaProvider, ok := model.(common.SchemaProvider); ok {
|
||||||
|
schema = schemaProvider.SchemaName()
|
||||||
|
} else {
|
||||||
|
schema = defaultSchema
|
||||||
}
|
}
|
||||||
return entity
|
|
||||||
|
return schema, tableName
|
||||||
|
}
|
||||||
|
|
||||||
|
// No TableNameProvider, so check for schema and use entity as table name
|
||||||
|
if schemaProvider, ok := model.(common.SchemaProvider); ok {
|
||||||
|
schema = schemaProvider.SchemaName()
|
||||||
|
} else {
|
||||||
|
schema = defaultSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to entity name as table
|
||||||
|
return schema, entity
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTableName returns the full table name including schema (schema.table)
|
||||||
|
func (h *Handler) getTableName(schema, entity string, model interface{}) string {
|
||||||
|
schemaName, tableName := h.getSchemaAndTable(schema, entity, model)
|
||||||
|
if schemaName != "" {
|
||||||
|
return fmt.Sprintf("%s.%s", schemaName, tableName)
|
||||||
|
}
|
||||||
|
return tableName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) generateMetadata(schema, entity string, model interface{}) *common.TableMetadata {
|
func (h *Handler) generateMetadata(schema, entity string, model interface{}) *common.TableMetadata {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user