diff --git a/SCHEMA_TABLE_HANDLING.md b/SCHEMA_TABLE_HANDLING.md new file mode 100644 index 0000000..999fa34 --- /dev/null +++ b/SCHEMA_TABLE_HANDLING.md @@ -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. diff --git a/pkg/common/adapters/database/bun.go b/pkg/common/adapters/database/bun.go index 1fedffd..e0a6a93 100644 --- a/pkg/common/adapters/database/bun.go +++ b/pkg/common/adapters/database/bun.go @@ -78,7 +78,8 @@ func (b *BunAdapter) RunInTransaction(ctx context.Context, fn func(common.Databa // BunSelectQuery implements SelectQuery for Bun type BunSelectQuery struct { query *bun.SelectQuery - tableName string + schema string // Separated schema name + tableName string // Just the table name, without schema 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 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 @@ -95,7 +98,8 @@ func (b *BunSelectQuery) Model(model interface{}) common.SelectQuery { func (b *BunSelectQuery) Table(table string) common.SelectQuery { 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 } @@ -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 != "" { 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 @@ -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 != "" { prefix = b.tableName - if idx := strings.LastIndex(prefix, "."); idx != -1 { - prefix = prefix[idx+1:] - } } // Construct LEFT JOIN with prefix diff --git a/pkg/common/adapters/database/gorm.go b/pkg/common/adapters/database/gorm.go index 4748cf4..d92a29e 100644 --- a/pkg/common/adapters/database/gorm.go +++ b/pkg/common/adapters/database/gorm.go @@ -70,7 +70,8 @@ func (g *GormAdapter) RunInTransaction(ctx context.Context, fn func(common.Datab // GormSelectQuery implements SelectQuery for GORM type GormSelectQuery struct { db *gorm.DB - tableName string + schema string // Separated schema name + tableName string // Just the table name, without schema 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 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 @@ -87,7 +90,8 @@ func (g *GormSelectQuery) Model(model interface{}) common.SelectQuery { func (g *GormSelectQuery) Table(table string) common.SelectQuery { 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 } @@ -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 != "" { 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 @@ -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 != "" { prefix = g.tableName - if idx := strings.LastIndex(prefix, "."); idx != -1 { - prefix = prefix[idx+1:] - } } // Construct LEFT JOIN with prefix diff --git a/pkg/common/adapters/database/utils.go b/pkg/common/adapters/database/utils.go new file mode 100644 index 0000000..e1e710d --- /dev/null +++ b/pkg/common/adapters/database/utils.go @@ -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 +} diff --git a/pkg/resolvespec/handler.go b/pkg/resolvespec/handler.go index e1ec586..67c16ff 100644 --- a/pkg/resolvespec/handler.go +++ b/pkg/resolvespec/handler.go @@ -469,11 +469,65 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti } } -func (h *Handler) getTableName(schema, entity string, model interface{}) string { - if provider, ok := model.(common.TableNameProvider); ok { - return provider.TableName() +// parseTableName splits a table name that may contain schema into separate schema and table +func (h *Handler) parseTableName(fullTableName string) (schema, table string) { + 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 { diff --git a/pkg/restheadspec/handler.go b/pkg/restheadspec/handler.go index 5e7247b..fbc0607 100644 --- a/pkg/restheadspec/handler.go +++ b/pkg/restheadspec/handler.go @@ -531,20 +531,65 @@ func (h *Handler) applyFilter(query common.SelectQuery, filter common.FilterOpti } } -func (h *Handler) getTableName(schema, entity string, model interface{}) string { - // Check if model implements TableNameProvider - if provider, ok := model.(common.TableNameProvider); ok { - tableName := provider.TableName() - if tableName != "" { - return tableName +// parseTableName splits a table name that may contain schema into separate schema and table +func (h *Handler) parseTableName(fullTableName string) (schema, table string) { + if idx := strings.LastIndex(fullTableName, "."); idx != -1 { + return fullTableName[:idx], fullTableName[idx+1:] + } + 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 } - // Default to schema.entity - if schema != "" { - return fmt.Sprintf("%s.%s", schema, entity) + // 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 } - return entity + + // 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 {