From 09d514a7b5cd2de500aabd29bd941b6deb3983c8 Mon Sep 17 00:00:00 2001 From: Petr Stepanek Date: Sun, 7 Sep 2025 13:45:30 +0200 Subject: [PATCH 1/7] oracle implementation and fixes - in progress --- internal/domain/models/config.go | 47 ++- internal/domain/models/database_config.go | 392 ++++++++++-------- .../persistence/mysql/database_repository.go | 5 +- 3 files changed, 258 insertions(+), 186 deletions(-) diff --git a/internal/domain/models/config.go b/internal/domain/models/config.go index e24ab26..e87d38e 100644 --- a/internal/domain/models/config.go +++ b/internal/domain/models/config.go @@ -95,6 +95,7 @@ type SecurityConfig struct { // SSLConfig represents SSL/TLS configuration for database connections type SSLConfig struct { Enabled bool `yaml:"enabled"` + Mode string `yaml:"mode,omitempty"` // PostgreSQL SSL mode CertFile string `yaml:"cert_file,omitempty"` KeyFile string `yaml:"key_file,omitempty"` CAFile string `yaml:"ca_file,omitempty"` @@ -190,7 +191,10 @@ func (c *Config) GetDatabaseConfig() DatabaseConfig { } // Fall back to legacy MySQL config for backward compatibility - return &c.MySQL + return DatabaseConfig{ + Type: DatabaseTypeMySQL, + MySQL: &c.MySQL, + } } // GetDatabaseType returns the active database type @@ -349,3 +353,44 @@ type AnimationConfig struct { AnimationSpeed float64 `yaml:"animation_speed"` ParticleCount int `yaml:"particle_count"` } + +// GetUsername returns the username, preferring Username field over User field +func (c *MySQLConfig) GetUsername() string { + if c.Username != "" { + return c.Username + } + return c.User +} + +// GetPassword returns the password +func (c *MySQLConfig) GetPassword() string { + return c.Password +} + +// GetHost returns the host +func (c *MySQLConfig) GetHost() string { + return c.Host +} + +// GetPort returns the port +func (c *MySQLConfig) GetPort() int { + return c.Port +} + +// GetDatabase returns the database name +func (c *MySQLConfig) GetDatabase() string { + return c.Database +} + +// GetUsername returns the username, preferring Username field over User field +func (c *PostgreSQLConfig) GetUsername() string { + if c.Username != "" { + return c.Username + } + return c.User +} + +// GetPassword returns the password +func (c *PostgreSQLConfig) GetPassword() string { + return c.Password +} diff --git a/internal/domain/models/database_config.go b/internal/domain/models/database_config.go index 2953ba2..996b4b4 100644 --- a/internal/domain/models/database_config.go +++ b/internal/domain/models/database_config.go @@ -11,251 +11,277 @@ package models -// DatabaseType represents the type of database engine +import ( + "fmt" + "strings" +) + +// DatabaseType represents supported database types type DatabaseType string const ( DatabaseTypeMySQL DatabaseType = "mysql" DatabaseTypePostgreSQL DatabaseType = "postgresql" + DatabaseTypeOracle DatabaseType = "oracle" ) -// DatabaseConfig represents generic database configuration interface -type DatabaseConfig interface { - GetDatabaseType() DatabaseType - GetHost() string - GetPort() int - GetUsername() string - GetPassword() string - GetDatabase() string - GetConnectionMode() ConnectionMode - GetDataFiltering() DataFilteringConfig - GetSecurity() SecurityConfig - GetSSLConfig() SSLConfig - GetAutoGeneratedRules() AutoGeneratedRulesConfig - Validate() error -} - -// PostgreSQLConfig represents PostgreSQL database connection configuration -type PostgreSQLConfig struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - User string `yaml:"user"` - Username string `yaml:"username,omitempty"` // alias for User - Password string `yaml:"password"` - Database string `yaml:"database"` - Schema string `yaml:"schema,omitempty"` // PostgreSQL-specific schema - ConnectionMode ConnectionMode `yaml:"connection_mode,omitempty"` - DataFiltering DataFilteringConfig `yaml:"data_filtering,omitempty"` - Security SecurityConfig `yaml:"security,omitempty"` - SSLConfig PostgreSQLSSLConfig `yaml:"ssl,omitempty"` - AutoGeneratedRules AutoGeneratedRulesConfig `yaml:"auto_generated_rules,omitempty"` - - // PostgreSQL-specific settings - SearchPath []string `yaml:"search_path,omitempty"` // Schema search path - ApplicationName string `yaml:"application_name,omitempty"` // Connection application name - StatementTimeout int `yaml:"statement_timeout,omitempty"` // Statement timeout in seconds +// DatabaseSelector represents configuration for choosing database type +type DatabaseSelector struct { + Type DatabaseType `yaml:"type"` // "mysql" or "postgresql" + MySQL *MySQLConfig `yaml:"mysql,omitempty"` + PostgreSQL *PostgreSQLConfig `yaml:"postgresql,omitempty"` + OracleConfig *OracleConfig `yaml:"oracle,omitempty"` } -// PostgreSQLSSLConfig represents PostgreSQL-specific SSL configuration -type PostgreSQLSSLConfig struct { - Mode string `yaml:"mode"` // disable, allow, prefer, require, verify-ca, verify-full - CertFile string `yaml:"cert_file,omitempty"` - KeyFile string `yaml:"key_file,omitempty"` - CAFile string `yaml:"ca_file,omitempty"` - CRLFile string `yaml:"crl_file,omitempty"` - ServerName string `yaml:"server_name,omitempty"` // For SNI - InsecureSkipVerify bool `yaml:"insecure_skip_verify,omitempty"` +// DatabaseConfig represents database connection configuration +type DatabaseConfig struct { + Type DatabaseType `yaml:"type" json:"type"` + MySQL *MySQLConfig `yaml:"mysql,omitempty" json:"mysql,omitempty"` + PostgreSQL *PostgreSQLConfig `yaml:"postgresql,omitempty" json:"postgresql,omitempty"` + Oracle *OracleConfig `yaml:"oracle,omitempty" json:"oracle,omitempty"` } -// DatabaseType methods for MySQLConfig -func (c *MySQLConfig) GetDatabaseType() DatabaseType { - return DatabaseTypeMySQL -} +// MySQLConfig represents MySQL database configuration -func (c *MySQLConfig) GetHost() string { - return c.Host +// PostgreSQLConfig represents PostgreSQL database configuration +type PostgreSQLConfig struct { + Host string `yaml:"host" json:"host"` + Port int `yaml:"port" json:"port"` + User string `yaml:"user" json:"user"` + Username string `yaml:"username" json:"username"` // Alternative field name + Password string `yaml:"password" json:"password"` + Database string `yaml:"database" json:"database"` + + SSLConfig SSLConfig `yaml:"ssl" json:"ssl"` + Security SecurityConfig `yaml:"security" json:"security"` + StatementTimeout int `yaml:"statement_timeout" json:"statement_timeout"` // seconds + ApplicationName string `yaml:"application_name" json:"application_name"` } -func (c *MySQLConfig) GetPort() int { - return c.Port +// OracleConfig represents Oracle Database configuration +type OracleConfig struct { + // Connection details + Host string `yaml:"host" json:"host"` + Port int `yaml:"port" json:"port"` + ServiceName string `yaml:"service_name" json:"service_name"` + SID string `yaml:"sid" json:"sid"` + DSN string `yaml:"dsn" json:"dsn"` // TNS connection descriptor + + // Authentication + Username string `yaml:"username" json:"username"` + Password string `yaml:"password" json:"password"` + + // Connection pool settings + MaxOpenConns int `yaml:"max_open_conns" json:"max_open_conns"` + MaxIdleConns int `yaml:"max_idle_conns" json:"max_idle_conns"` + ConnMaxLifetime int `yaml:"conn_max_lifetime" json:"conn_max_lifetime"` // minutes + + // Timeout settings + ConnectionTimeout int `yaml:"connection_timeout" json:"connection_timeout"` // seconds + QueryTimeout int `yaml:"query_timeout" json:"query_timeout"` // seconds + + // Oracle specific + Timezone string `yaml:"timezone" json:"timezone"` + WalletLocation string `yaml:"wallet_location" json:"wallet_location"` + ApplicationName string `yaml:"application_name" json:"application_name"` + + // Security + Security SecurityConfig `yaml:"security" json:"security"` } -func (c *MySQLConfig) GetUsername() string { - if c.Username != "" { - return c.Username +// GetActiveConfig returns the active database configuration based on the selected type +func (ds *DatabaseSelector) GetActiveConfig() DatabaseConfig { + switch ds.Type { + case DatabaseTypeMySQL: + if ds.MySQL != nil { + return DatabaseConfig{ + Type: DatabaseTypeMySQL, + MySQL: ds.MySQL, + } + } + case DatabaseTypePostgreSQL: + if ds.PostgreSQL != nil { + return DatabaseConfig{ + Type: DatabaseTypePostgreSQL, + PostgreSQL: ds.PostgreSQL, + } + } + case DatabaseTypeOracle: + if ds.OracleConfig != nil { + return DatabaseConfig{ + Type: DatabaseTypeOracle, + Oracle: ds.OracleConfig, + } + } } - return c.User -} -func (c *MySQLConfig) GetPassword() string { - return c.Password + // Return empty config if no valid configuration is found + return DatabaseConfig{} } -func (c *MySQLConfig) GetDatabase() string { - return c.Database -} - -func (c *MySQLConfig) GetConnectionMode() ConnectionMode { - return c.ConnectionMode -} +// BuildConnectionString creates Oracle connection string +func (c *OracleConfig) BuildConnectionString() string { + // If DSN is provided, use it directly (TNS descriptor) + if c.DSN != "" { + return fmt.Sprintf("oracle://%s:%s@%s", c.Username, c.Password, c.DSN) + } -func (c *MySQLConfig) GetDataFiltering() DataFilteringConfig { - return c.DataFiltering -} + // Build connection string for simple connection + var connParts []string + + // Use service name if provided, otherwise SID + if c.ServiceName != "" { + connParts = append(connParts, fmt.Sprintf("oracle://%s:%s@%s:%d/%s", + c.Username, c.Password, c.Host, c.Port, c.ServiceName)) + } else if c.SID != "" { + // For SID connections, use different format + connParts = append(connParts, fmt.Sprintf("oracle://%s:%s@%s:%d/%s?SID=%s", + c.Username, c.Password, c.Host, c.Port, c.SID, c.SID)) + } else { + // Default to XE service + connParts = append(connParts, fmt.Sprintf("oracle://%s:%s@%s:%d/XE", + c.Username, c.Password, c.Host, c.Port)) + } -func (c *MySQLConfig) GetSecurity() SecurityConfig { - return c.Security -} + connString := connParts[0] -func (c *MySQLConfig) GetSSLConfig() SSLConfig { - return c.SSLConfig -} + // Add additional parameters + params := make([]string, 0) -func (c *MySQLConfig) GetAutoGeneratedRules() AutoGeneratedRulesConfig { - return c.AutoGeneratedRules -} - -func (c *MySQLConfig) Validate() error { - if c.Host == "" { - return NewValidationError("mysql.host", "Host is required") - } - if c.GetUsername() == "" { - return NewValidationError("mysql.user", "Username is required") + if c.ConnectionTimeout > 0 { + params = append(params, fmt.Sprintf("TIMEOUT=%d", c.ConnectionTimeout)) } - if c.Database == "" { - return NewValidationError("mysql.database", "Database name is required") - } - if c.Port <= 0 || c.Port > 65535 { - return NewValidationError("mysql.port", "Port must be between 1 and 65535") + + if c.Timezone != "" { + params = append(params, fmt.Sprintf("TIMEZONE=%s", c.Timezone)) } - return nil -} -// DatabaseType methods for PostgreSQLConfig -func (c *PostgreSQLConfig) GetDatabaseType() DatabaseType { - return DatabaseTypePostgreSQL -} + if c.WalletLocation != "" { + params = append(params, fmt.Sprintf("WALLET=%s", c.WalletLocation)) + } -func (c *PostgreSQLConfig) GetHost() string { - return c.Host -} + if len(params) > 0 { + if strings.Contains(connString, "?") { + connString += "&" + strings.Join(params, "&") + } else { + connString += "?" + strings.Join(params, "&") + } + } -func (c *PostgreSQLConfig) GetPort() int { - return c.Port + return connString } -func (c *PostgreSQLConfig) GetUsername() string { - if c.Username != "" { - return c.Username +// Validate checks if Oracle configuration is valid +func (c *OracleConfig) Validate() error { + if c.Username == "" { + return fmt.Errorf("oracle username is required") } - return c.User -} -func (c *PostgreSQLConfig) GetPassword() string { - return c.Password -} + if c.Password == "" { + return fmt.Errorf("oracle password is required") + } -func (c *PostgreSQLConfig) GetDatabase() string { - return c.Database -} + // If DSN is not provided, check basic connection info + if c.DSN == "" { + if c.Host == "" { + return fmt.Errorf("oracle host is required when DSN is not provided") + } -func (c *PostgreSQLConfig) GetConnectionMode() ConnectionMode { - return c.ConnectionMode -} + if c.Port <= 0 { + c.Port = 1521 // Default Oracle port + } -func (c *PostgreSQLConfig) GetDataFiltering() DataFilteringConfig { - return c.DataFiltering -} + if c.ServiceName == "" && c.SID == "" { + c.ServiceName = "XE" // Default to Oracle XE + } + } -func (c *PostgreSQLConfig) GetSecurity() SecurityConfig { - return c.Security -} + // Set defaults + if c.MaxOpenConns <= 0 { + c.MaxOpenConns = 10 + } -func (c *PostgreSQLConfig) GetSSLConfig() SSLConfig { - // Convert PostgreSQL SSL config to generic SSL config - return SSLConfig{ - Enabled: c.SSLConfig.Mode != "disable", - CertFile: c.SSLConfig.CertFile, - KeyFile: c.SSLConfig.KeyFile, - CAFile: c.SSLConfig.CAFile, - InsecureSkipVerify: c.SSLConfig.InsecureSkipVerify, + if c.MaxIdleConns <= 0 { + c.MaxIdleConns = 5 } -} -func (c *PostgreSQLConfig) GetAutoGeneratedRules() AutoGeneratedRulesConfig { - return c.AutoGeneratedRules -} + if c.ConnectionTimeout <= 0 { + c.ConnectionTimeout = 30 + } -func (c *PostgreSQLConfig) Validate() error { - if c.Host == "" { - return NewValidationError("postgresql.host", "Host is required") + if c.QueryTimeout <= 0 { + c.QueryTimeout = 30 } - if c.GetUsername() == "" { - return NewValidationError("postgresql.user", "Username is required") + + if c.ApplicationName == "" { + c.ApplicationName = "sql-graph-visualizer" } - if c.Database == "" { - return NewValidationError("postgresql.database", "Database name is required") + + // Set security defaults + if c.Security.MaxConnections <= 0 { + c.Security.MaxConnections = c.MaxOpenConns } - if c.Port <= 0 || c.Port > 65535 { - return NewValidationError("postgresql.port", "Port must be between 1 and 65535") + + if c.Security.ConnectionTimeout <= 0 { + c.Security.ConnectionTimeout = c.ConnectionTimeout } - // Validate SSL mode - validSSLModes := []string{"disable", "allow", "prefer", "require", "verify-ca", "verify-full"} - if c.SSLConfig.Mode != "" { - valid := false - for _, mode := range validSSLModes { - if c.SSLConfig.Mode == mode { - valid = true - break - } - } - if !valid { - return NewValidationError("postgresql.ssl.mode", "Invalid SSL mode") - } + if c.Security.QueryTimeout <= 0 { + c.Security.QueryTimeout = c.QueryTimeout } return nil } -// NewValidationError creates a new validation error -func NewValidationError(field, message string) *ValidationError { - return &ValidationError{ - Type: "VALIDATION", - Message: field + ": " + message, +// GetEffectiveConfig returns the active database config based on type +func (d *DatabaseConfig) GetEffectiveConfig() interface{} { + switch d.Type { + case DatabaseTypeMySQL: + return d.MySQL + case DatabaseTypePostgreSQL: + return d.PostgreSQL + case DatabaseTypeOracle: + return d.Oracle + default: + return nil } } -// DatabaseSelector represents configuration for choosing database type -type DatabaseSelector struct { - Type DatabaseType `yaml:"type"` // "mysql" or "postgresql" - MySQL *MySQLConfig `yaml:"mysql,omitempty"` - PostgreSQL *PostgreSQLConfig `yaml:"postgresql,omitempty"` -} - -func (ds *DatabaseSelector) GetActiveConfig() DatabaseConfig { - switch ds.Type { +// Validate validates the database configuration +func (d *DatabaseConfig) Validate() error { + switch d.Type { case DatabaseTypeMySQL: - if ds.MySQL != nil { - return ds.MySQL + if d.MySQL == nil { + return fmt.Errorf("mysql configuration is required when type is mysql") } + // Add MySQL-specific validation if needed case DatabaseTypePostgreSQL: - if ds.PostgreSQL != nil { - return ds.PostgreSQL + if d.PostgreSQL == nil { + return fmt.Errorf("postgresql configuration is required when type is postgresql") } + // Add PostgreSQL-specific validation if needed + case DatabaseTypeOracle: + if d.Oracle == nil { + return fmt.Errorf("oracle configuration is required when type is oracle") + } + return d.Oracle.Validate() + default: + return fmt.Errorf("unsupported database type: %s", d.Type) } return nil } -func (ds *DatabaseSelector) Validate() error { - if ds.Type == "" { - return NewValidationError("database.type", "Database type must be specified") - } - - config := ds.GetActiveConfig() - if config == nil { - return NewValidationError("database", "No configuration found for database type: "+string(ds.Type)) +// IsSupported checks if the database type is supported +func (dt DatabaseType) IsSupported() bool { + switch dt { + case DatabaseTypeMySQL, DatabaseTypePostgreSQL, DatabaseTypeOracle: + return true + default: + return false } +} - return config.Validate() +// String returns string representation of database type +func (dt DatabaseType) String() string { + return string(dt) } diff --git a/internal/infrastructure/persistence/mysql/database_repository.go b/internal/infrastructure/persistence/mysql/database_repository.go index 18a9285..d7939c9 100644 --- a/internal/infrastructure/persistence/mysql/database_repository.go +++ b/internal/infrastructure/persistence/mysql/database_repository.go @@ -34,11 +34,12 @@ func NewMySQLDatabaseRepository() repository.DatabaseRepository { return &MySQLDatabaseRepository{} } +// Connect establishes connection to MySQL database // Connect establishes connection to MySQL database func (r *MySQLDatabaseRepository) Connect(ctx context.Context, config models.DatabaseConfig) (*sql.DB, error) { - mysqlConfig, ok := config.(*models.MySQLConfig) + mysqlConfig, ok := config.GetEffectiveConfig().(*models.MySQLConfig) if !ok { - return nil, fmt.Errorf("expected MySQLConfig, got %T", config) + return nil, fmt.Errorf("expected MySQLConfig, got %T", config.GetEffectiveConfig()) } // Use Username if set, otherwise fallback to User From 1d9646601406e34f2183fd3f07decf0373f97d0f Mon Sep 17 00:00:00 2001 From: Petr Stepanek Date: Tue, 14 Apr 2026 14:14:15 +0300 Subject: [PATCH 2/7] Oracle driver dependency; Oracle repository, factory and CLI implementation --- cmd/sql-graph-cli/commands/analyze.go | 21 +- cmd/sql-graph-cli/commands/test.go | 21 +- internal/domain/models/database_config.go | 3 +- .../factories/database_repository_factory.go | 4 + .../persistence/oracle/database_repository.go | 517 ++++++++++++++++++ 5 files changed, 563 insertions(+), 3 deletions(-) create mode 100644 internal/infrastructure/persistence/oracle/database_repository.go diff --git a/cmd/sql-graph-cli/commands/analyze.go b/cmd/sql-graph-cli/commands/analyze.go index 5a3b1ba..c3a6cb6 100644 --- a/cmd/sql-graph-cli/commands/analyze.go +++ b/cmd/sql-graph-cli/commands/analyze.go @@ -106,7 +106,7 @@ Supports both MySQL and PostgreSQL databases with database-specific optimization } // Database type and connection flags - cmd.Flags().StringVar(&dbType, "db-type", "mysql", "Database type: mysql, postgresql") + cmd.Flags().StringVar(&dbType, "db-type", "mysql", "Database type: mysql, postgresql, oracle") cmd.Flags().StringVar(&host, "host", "localhost", "Database host") cmd.Flags().IntVar(&port, "port", 0, "Database port (0 = auto-detect: MySQL=3306, PostgreSQL=5432)") cmd.Flags().StringVar(&username, "username", "", "Database username") @@ -281,6 +281,25 @@ func runAnalyze(_ *cobra.Command, opts analyzeOptions) error { }, }} + case models.DatabaseTypeOracle: + port := opts.Port + if port == 0 { + port = 1521 + } + config = models.DatabaseConfig{Type: models.DatabaseTypeOracle, Oracle: &models.OracleConfig{ + Host: opts.Host, + Port: port, + ServiceName: opts.Database, + Username: opts.Username, + Password: opts.Password, + Security: models.SecurityConfig{ + ReadOnly: true, + ConnectionTimeout: opts.ConnectionTimeout, + QueryTimeout: opts.QueryTimeout, + MaxConnections: opts.MaxConnections, + }, + }} + default: return fmt.Errorf("unsupported database type: %s", opts.DBType) } diff --git a/cmd/sql-graph-cli/commands/test.go b/cmd/sql-graph-cli/commands/test.go index 71e5fec..6e9b086 100644 --- a/cmd/sql-graph-cli/commands/test.go +++ b/cmd/sql-graph-cli/commands/test.go @@ -90,7 +90,7 @@ This command provides immediate feedback on: } // Database type and connection flags - cmd.Flags().StringVar(&dbType, "db-type", "mysql", "Database type: mysql, postgresql") + cmd.Flags().StringVar(&dbType, "db-type", "mysql", "Database type: mysql, postgresql, oracle") cmd.Flags().StringVar(&host, "host", "localhost", "Database host") cmd.Flags().IntVar(&port, "port", 0, "Database port (0 = auto-detect: MySQL=3306, PostgreSQL=5432)") cmd.Flags().StringVar(&username, "username", "", "Database username") @@ -215,6 +215,25 @@ func runTest(opts testOptions) error { }, }} + case models.DatabaseTypeOracle: + port := opts.Port + if port == 0 { + port = 1521 + } + config = models.DatabaseConfig{Type: models.DatabaseTypeOracle, Oracle: &models.OracleConfig{ + Host: opts.Host, + Port: port, + ServiceName: opts.Database, + Username: opts.Username, + Password: opts.Password, + Security: models.SecurityConfig{ + ReadOnly: true, + ConnectionTimeout: opts.ConnectionTimeout, + QueryTimeout: 30, + MaxConnections: 1, + }, + }} + default: return fmt.Errorf("unsupported database type: %s", opts.DBType) } diff --git a/internal/domain/models/database_config.go b/internal/domain/models/database_config.go index 792aad9..588f6a7 100644 --- a/internal/domain/models/database_config.go +++ b/internal/domain/models/database_config.go @@ -24,7 +24,8 @@ const ( DatabaseTypeMySQL DatabaseType = "mysql" // DatabaseTypePostgreSQL represents PostgreSQL database type DatabaseTypePostgreSQL DatabaseType = "postgresql" - DatabaseTypeOracle DatabaseType = "oracle" + // DatabaseTypeOracle represents Oracle database type + DatabaseTypeOracle DatabaseType = "oracle" ) // DatabaseSelector represents configuration for choosing database type diff --git a/internal/infrastructure/factories/database_repository_factory.go b/internal/infrastructure/factories/database_repository_factory.go index 7e7e68b..706a489 100644 --- a/internal/infrastructure/factories/database_repository_factory.go +++ b/internal/infrastructure/factories/database_repository_factory.go @@ -17,6 +17,7 @@ import ( "sql-graph-visualizer/internal/domain/models" "sql-graph-visualizer/internal/domain/repositories" "sql-graph-visualizer/internal/infrastructure/persistence/mysql" + "sql-graph-visualizer/internal/infrastructure/persistence/oracle" "sql-graph-visualizer/internal/infrastructure/persistence/postgresql" ) @@ -35,6 +36,8 @@ func (f *DatabaseRepositoryFactory) CreateRepository(dbType models.DatabaseType) return mysql.NewMySQLDatabaseRepository(), nil case models.DatabaseTypePostgreSQL: return postgresql.NewPostgreSQLDatabaseRepository(), nil + case models.DatabaseTypeOracle: + return oracle.NewOracleDatabaseRepository(), nil default: return nil, fmt.Errorf("unsupported database type: %s", dbType) } @@ -45,5 +48,6 @@ func (f *DatabaseRepositoryFactory) GetSupportedDatabaseTypes() []models.Databas return []models.DatabaseType{ models.DatabaseTypeMySQL, models.DatabaseTypePostgreSQL, + models.DatabaseTypeOracle, } } diff --git a/internal/infrastructure/persistence/oracle/database_repository.go b/internal/infrastructure/persistence/oracle/database_repository.go new file mode 100644 index 0000000..4080497 --- /dev/null +++ b/internal/infrastructure/persistence/oracle/database_repository.go @@ -0,0 +1,517 @@ +/* + * Copyright (c) 2025 Petr Miroslav Stepanek + * + * This source code is licensed under a Dual License: + * - AGPL-3.0 for open source use (see LICENSE file) + * - Commercial License for business use (contact: petrstepanek99@gmail.com) + * + * This software contains patent-pending innovations in database analysis + * and graph visualization. Commercial use requires separate licensing. + */ + +// Package oracle provides Oracle database persistence using the go-ora pure Go driver. +package oracle + +import ( + "context" + "database/sql" + "fmt" + "sql-graph-visualizer/internal/domain/models" + "sql-graph-visualizer/internal/domain/repositories" + "strings" + "time" + + _ "github.com/sijms/go-ora/v2" // Oracle driver registration + "github.com/sirupsen/logrus" +) + +// OracleDatabaseRepository implements DatabaseRepository for Oracle Database. +// +//nolint:revive // OracleDatabaseRepository is descriptive and follows project conventions +type OracleDatabaseRepository struct { + db *sql.DB +} + +// NewOracleDatabaseRepository creates a new Oracle database repository. +func NewOracleDatabaseRepository() repositories.DatabaseRepository { + return &OracleDatabaseRepository{} +} + +// Connect establishes connection to Oracle database. +func (r *OracleDatabaseRepository) Connect(ctx context.Context, config models.DatabaseConfig) (*sql.DB, error) { + oracleConfig, ok := config.GetEffectiveConfig().(*models.OracleConfig) + if !ok { + return nil, fmt.Errorf("expected OracleConfig, got %T", config.GetEffectiveConfig()) + } + + if err := oracleConfig.Validate(); err != nil { + return nil, fmt.Errorf("invalid Oracle configuration: %w", err) + } + + connString := oracleConfig.BuildConnectionString() + + logrus.Infof("Connecting to Oracle database: %s@%s:%d/%s", + oracleConfig.Username, oracleConfig.Host, oracleConfig.Port, oracleConfig.ServiceName) + + db, err := sql.Open("oracle", connString) + if err != nil { + return nil, fmt.Errorf("failed to open Oracle database connection: %w", err) + } + + db.SetMaxOpenConns(oracleConfig.MaxOpenConns) + db.SetMaxIdleConns(oracleConfig.MaxIdleConns) + db.SetConnMaxLifetime(time.Duration(oracleConfig.ConnMaxLifetime) * time.Minute) + + ctxTimeout, cancel := context.WithTimeout(ctx, time.Duration(oracleConfig.ConnectionTimeout)*time.Second) + defer cancel() + + if err := db.PingContext(ctxTimeout); err != nil { + return nil, fmt.Errorf("failed to ping Oracle database: %w", err) + } + + r.db = db + logrus.Info("Successfully connected to Oracle database") + return db, nil +} + +// Close closes the database connection. +func (r *OracleDatabaseRepository) Close() error { + if r.db != nil { + return r.db.Close() + } + return nil +} + +// TestConnection tests the database connection. +func (r *OracleDatabaseRepository) TestConnection(ctx context.Context) error { + if r.db == nil { + return fmt.Errorf("no active database connection") + } + return r.db.PingContext(ctx) +} + +// GetTables returns list of tables based on filtering configuration. +func (r *OracleDatabaseRepository) GetTables(ctx context.Context, filters models.DataFilteringConfig) ([]string, error) { + if r.db == nil { + return nil, fmt.Errorf("no active database connection") + } + + query := ` + SELECT table_name + FROM all_tables + WHERE owner = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') + ORDER BY table_name + ` + + rows, err := r.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer closeRows(rows) + + var allTables []string + for rows.Next() { + var tableName string + if err := rows.Scan(&tableName); err != nil { + return nil, err + } + allTables = append(allTables, tableName) + } + + return applyTableFiltering(allTables, filters), nil +} + +// GetColumns retrieves column information for an Oracle table. +func (r *OracleDatabaseRepository) GetColumns(ctx context.Context, tableName string) ([]*models.ColumnInfo, error) { + if r.db == nil { + return nil, fmt.Errorf("no active database connection") + } + + query := ` + SELECT + c.column_name, + c.data_type, + CASE WHEN c.nullable = 'Y' THEN 'YES' ELSE 'NO' END AS is_nullable, + NVL(c.data_default, '') AS column_default, + CASE + WHEN cc.constraint_type = 'P' THEN 'PRIMARY' + WHEN cc.constraint_type = 'U' THEN 'UNIQUE' + ELSE '' + END AS key_type, + CASE WHEN c.identity_column = 'YES' THEN 'auto_increment' ELSE '' END AS extra + FROM all_tab_columns c + LEFT JOIN ( + SELECT col.column_name, con.constraint_type + FROM all_cons_columns col + JOIN all_constraints con ON col.constraint_name = con.constraint_name + AND col.owner = con.owner + WHERE con.table_name = :1 + AND con.owner = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') + AND con.constraint_type IN ('P', 'U') + ) cc ON c.column_name = cc.column_name + WHERE c.table_name = :2 + AND c.owner = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') + ORDER BY c.column_id + ` + + rows, err := r.db.QueryContext(ctx, query, tableName, tableName) + if err != nil { + return nil, err + } + defer closeRows(rows) + + var columns []*models.ColumnInfo + for rows.Next() { + var col models.ColumnInfo + var defaultVal, keyType sql.NullString + + err := rows.Scan( + &col.Name, + &col.DataType, + &col.IsNullable, + &defaultVal, + &keyType, + &col.Extra, + ) + if err != nil { + return nil, err + } + + if defaultVal.Valid { + col.DefaultValue = strings.TrimSpace(defaultVal.String) + } + if keyType.Valid { + col.KeyType = keyType.String + col.IsKey = keyType.String != "" + } + + columns = append(columns, &col) + } + + return columns, nil +} + +// GetForeignKeys retrieves foreign key information for an Oracle table. +func (r *OracleDatabaseRepository) GetForeignKeys(ctx context.Context, tableName string) ([]models.ForeignKeyInfo, error) { + if r.db == nil { + return nil, fmt.Errorf("no active database connection") + } + + query := ` + SELECT + a.constraint_name, + a.column_name, + c_pk.table_name AS referenced_table, + b.column_name AS referenced_column + FROM all_cons_columns a + JOIN all_constraints c ON a.constraint_name = c.constraint_name AND a.owner = c.owner + JOIN all_constraints c_pk ON c.r_constraint_name = c_pk.constraint_name AND c.r_owner = c_pk.owner + JOIN all_cons_columns b ON c_pk.constraint_name = b.constraint_name AND c_pk.owner = b.owner AND a.position = b.position + WHERE c.constraint_type = 'R' + AND a.table_name = :1 + AND a.owner = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') + ORDER BY a.constraint_name, a.position + ` + + rows, err := r.db.QueryContext(ctx, query, tableName) + if err != nil { + return nil, err + } + defer closeRows(rows) + + var foreignKeys []models.ForeignKeyInfo + for rows.Next() { + var fk models.ForeignKeyInfo + var constraintName string + err := rows.Scan( + &constraintName, + &fk.Column, + &fk.ReferencedTable, + &fk.ReferencedColumn, + ) + if err != nil { + return nil, err + } + fk.Name = constraintName + foreignKeys = append(foreignKeys, fk) + } + + return foreignKeys, nil +} + +// GetIndexes retrieves index information for an Oracle table. +func (r *OracleDatabaseRepository) GetIndexes(ctx context.Context, tableName string) ([]models.IndexInfo, error) { + if r.db == nil { + return nil, fmt.Errorf("no active database connection") + } + + query := ` + SELECT + i.index_name, + ic.column_name, + CASE WHEN i.uniqueness = 'UNIQUE' THEN 1 ELSE 0 END AS is_unique, + i.index_type + FROM all_indexes i + JOIN all_ind_columns ic ON i.index_name = ic.index_name AND i.owner = ic.index_owner + WHERE i.table_name = :1 + AND i.owner = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') + ORDER BY i.index_name, ic.column_position + ` + + rows, err := r.db.QueryContext(ctx, query, tableName) + if err != nil { + return nil, err + } + defer closeRows(rows) + + indexMap := make(map[string]*models.IndexInfo) + for rows.Next() { + var indexName, columnName, indexType string + var isUnique bool + + if err := rows.Scan(&indexName, &columnName, &isUnique, &indexType); err != nil { + return nil, err + } + + if idx, exists := indexMap[indexName]; exists { + idx.Columns = append(idx.Columns, columnName) + } else { + indexMap[indexName] = &models.IndexInfo{ + Name: indexName, + Columns: []string{columnName}, + IsUnique: isUnique, + Type: indexType, + } + } + } + + var indexes []models.IndexInfo + for _, idx := range indexMap { + indexes = append(indexes, *idx) + } + + return indexes, nil +} + +// GetConstraints retrieves constraint information for an Oracle table. +func (r *OracleDatabaseRepository) GetConstraints(ctx context.Context, tableName string) ([]models.Constraint, error) { + if r.db == nil { + return nil, fmt.Errorf("no active database connection") + } + + query := ` + SELECT + constraint_name, + constraint_type, + NVL(search_condition, '') AS condition + FROM all_constraints + WHERE table_name = :1 + AND owner = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') + ORDER BY constraint_name + ` + + rows, err := r.db.QueryContext(ctx, query, tableName) + if err != nil { + return nil, err + } + defer closeRows(rows) + + var constraints []models.Constraint + for rows.Next() { + var constraint models.Constraint + var condition sql.NullString + + if err := rows.Scan(&constraint.Name, &constraint.Type, &condition); err != nil { + return nil, err + } + + constraint.TableName = tableName + if condition.Valid { + constraint.Condition = condition.String + } + + constraints = append(constraints, constraint) + } + + return constraints, nil +} + +// GetDatabaseName returns the current Oracle database name. +func (r *OracleDatabaseRepository) GetDatabaseName(ctx context.Context) (string, error) { + if r.db == nil { + return "", fmt.Errorf("no active database connection") + } + + var dbName string + err := r.db.QueryRowContext(ctx, "SELECT ORA_DATABASE_NAME FROM DUAL").Scan(&dbName) + return dbName, err +} + +// GetDatabaseVersion returns the Oracle database version. +func (r *OracleDatabaseRepository) GetDatabaseVersion(ctx context.Context) (string, error) { + if r.db == nil { + return "", fmt.Errorf("no active database connection") + } + + var version string + err := r.db.QueryRowContext(ctx, "SELECT banner FROM v$version WHERE ROWNUM = 1").Scan(&version) + return version, err +} + +// GetSchemaNames returns list of accessible schemas in Oracle. +func (r *OracleDatabaseRepository) GetSchemaNames(ctx context.Context) ([]string, error) { + if r.db == nil { + return nil, fmt.Errorf("no active database connection") + } + + query := ` + SELECT DISTINCT owner + FROM all_tables + WHERE owner NOT IN ('SYS', 'SYSTEM', 'DBSNMP', 'OUTLN', 'XDB', + 'CTXSYS', 'MDSYS', 'OLAPSYS', 'WMSYS', 'EXFSYS', 'ORDSYS', + 'ORDDATA', 'APPQOSSYS', 'ANONYMOUS', 'GSMADMIN_INTERNAL') + ORDER BY owner + ` + + rows, err := r.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer closeRows(rows) + + var schemas []string + for rows.Next() { + var schema string + if err := rows.Scan(&schema); err != nil { + return nil, err + } + schemas = append(schemas, schema) + } + + return schemas, nil +} + +// GetTableRowCount returns the estimated row count for an Oracle table. +func (r *OracleDatabaseRepository) GetTableRowCount(ctx context.Context, tableName string) (int64, error) { + if r.db == nil { + return 0, fmt.Errorf("no active database connection") + } + + // Use statistics for fast estimate + query := ` + SELECT NVL(num_rows, 0) + FROM all_tables + WHERE table_name = :1 + AND owner = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') + ` + + var rowCount sql.NullInt64 + err := r.db.QueryRowContext(ctx, query, tableName).Scan(&rowCount) + if err != nil || !rowCount.Valid { + // Fall back to COUNT(*) + // #nosec G201 - tableName is escaped using EscapeIdentifier + countQuery := fmt.Sprintf("SELECT COUNT(*) FROM %s", r.EscapeIdentifier(tableName)) + err = r.db.QueryRowContext(ctx, countQuery).Scan(&rowCount) + if err != nil { + return 0, err + } + } + + if rowCount.Valid { + return rowCount.Int64, nil + } + + return 0, nil +} + +// SampleTableData retrieves a sample of data from the specified table. +func (r *OracleDatabaseRepository) SampleTableData(_ context.Context, _ string, _ int) ([]map[string]interface{}, error) { + return nil, fmt.Errorf("not implemented yet") +} + +// AnalyzeColumnStatistics analyzes statistical information for table columns. +func (r *OracleDatabaseRepository) AnalyzeColumnStatistics(_ context.Context, _, _ string) (*models.ColumnStatistics, error) { + return nil, fmt.Errorf("not implemented yet") +} + +// GetTableSize returns the size of the specified table. +func (r *OracleDatabaseRepository) GetTableSize(_ context.Context, _ string) (*models.TableSize, error) { + return nil, fmt.Errorf("not implemented yet") +} + +// GetQueryExecutionPlan returns the execution plan for the given query. +func (r *OracleDatabaseRepository) GetQueryExecutionPlan(_ context.Context, _ string) (string, error) { + return "", fmt.Errorf("not implemented yet") +} + +// ValidatePermissions validates database connection permissions. +func (r *OracleDatabaseRepository) ValidatePermissions(_ context.Context, _ []string) error { + return fmt.Errorf("not implemented yet") +} + +// CheckUserPrivileges checks user privileges for database operations. +func (r *OracleDatabaseRepository) CheckUserPrivileges(_ context.Context) (*models.UserPrivileges, error) { + return nil, fmt.Errorf("not implemented yet") +} + +// EscapeIdentifier escapes Oracle database identifiers. +func (r *OracleDatabaseRepository) EscapeIdentifier(identifier string) string { + return fmt.Sprintf(`"%s"`, strings.ReplaceAll(identifier, `"`, `""`)) +} + +// GetQuoteChar returns the character used for quoting identifiers in Oracle. +func (r *OracleDatabaseRepository) GetQuoteChar() string { + return `"` +} + +// GetDatabaseType returns the Oracle database type. +func (r *OracleDatabaseRepository) GetDatabaseType() models.DatabaseType { + return models.DatabaseTypeOracle +} + +// GetConnectionString builds Oracle connection string from config. +func (r *OracleDatabaseRepository) GetConnectionString(config models.DatabaseConfig) string { + oracleConfig, ok := config.GetEffectiveConfig().(*models.OracleConfig) + if !ok { + return "" + } + return oracleConfig.BuildConnectionString() +} + +// Helper functions + +func closeRows(rows *sql.Rows) { + if err := rows.Close(); err != nil { + logrus.WithError(err).Error("Failed to close rows") + } +} + +func applyTableFiltering(tables []string, filters models.DataFilteringConfig) []string { + if len(filters.TableBlacklist) == 0 && len(filters.TableWhitelist) == 0 { + return tables + } + + var filtered []string + for _, table := range tables { + if isInList(table, filters.TableBlacklist) { + continue + } + if len(filters.TableWhitelist) > 0 { + if !isInList(table, filters.TableWhitelist) { + continue + } + } + filtered = append(filtered, table) + } + + return filtered +} + +func isInList(item string, list []string) bool { + for _, listItem := range list { + if strings.EqualFold(item, listItem) { + return true + } + } + return false +} From c3cae6d8110c3637e0a2c0d20397bba1deeb2955 Mon Sep 17 00:00:00 2001 From: Petr Stepanek Date: Tue, 14 Apr 2026 14:14:33 +0300 Subject: [PATCH 3/7] dependencies update --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index ef59627..eec75a5 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/lib/pq v1.10.9 github.com/neo4j/neo4j-go-driver/v4 v4.4.7 github.com/rs/cors v1.11.1 + github.com/sijms/go-ora/v2 v2.9.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.10.1 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 9d900dd..74c3d64 100644 --- a/go.sum +++ b/go.sum @@ -85,6 +85,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sijms/go-ora/v2 v2.9.0 h1:+iQbUeTeCOFMb5BsOMgUhV8KWyrv9yjKpcK4x7+MFrg= +github.com/sijms/go-ora/v2 v2.9.0/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYoCqhR2dU= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= From cb9c377bf0c8763bb4d4707cccd984dda634ce34 Mon Sep 17 00:00:00 2001 From: Petr Stepanek Date: Tue, 14 Apr 2026 14:15:18 +0300 Subject: [PATCH 4/7] Oracle config --- config/config-oracle.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 config/config-oracle.yml diff --git a/config/config-oracle.yml b/config/config-oracle.yml new file mode 100644 index 0000000..5eab655 --- /dev/null +++ b/config/config-oracle.yml @@ -0,0 +1,35 @@ +# Oracle Database Configuration Example +# For use with SQL Graph Visualizer + +database: + type: "oracle" + oracle: + # Simple connection method + host: "localhost" + port: 1521 + service_name: "XEPDB1" # Or use sid: "XE" + # dsn: "(DESCRIPTION=...)" # TNS descriptor (alternative) + + username: "hr" + password: "password" + + # Connection pool + max_open_conns: 10 + max_idle_conns: 5 + conn_max_lifetime: 60 # minutes + + # Timeouts + connection_timeout: 30 # seconds + query_timeout: 30 # seconds + + # Optional + timezone: "UTC" + application_name: "sql-graph-visualizer" + # wallet_location: "/path/to/wallet" # For Oracle Wallet auth + +neo4j: + uri: "bolt://localhost:7687" + user: "neo4j" + password: "password" + +# Transformation rules will be auto-generated from Oracle schema From f8b85a60a4c1d5a0069eea5833c09cdc5351ce18 Mon Sep 17 00:00:00 2001 From: Petr Stepanek Date: Tue, 14 Apr 2026 14:15:41 +0300 Subject: [PATCH 5/7] Oracle docker setup --- docker-compose.oracle.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docker-compose.oracle.yml diff --git a/docker-compose.oracle.yml b/docker-compose.oracle.yml new file mode 100644 index 0000000..cb471fa --- /dev/null +++ b/docker-compose.oracle.yml @@ -0,0 +1,39 @@ +# Docker Compose for Oracle XE development/testing +# Usage: docker-compose -f docker-compose.oracle.yml up -d + +version: "3.8" + +services: + oracle-xe: + image: gvenzl/oracle-xe:21-slim + container_name: sql-graph-oracle-xe + environment: + ORACLE_PASSWORD: password + APP_USER: hr + APP_USER_PASSWORD: password + ports: + - "1521:1521" + volumes: + - oracle-data:/opt/oracle/oradata + healthcheck: + test: ["CMD", "healthcheck.sh"] + interval: 30s + timeout: 10s + retries: 10 + start_period: 120s + + neo4j: + image: neo4j:4.4 + container_name: sql-graph-neo4j-oracle + environment: + NEO4J_AUTH: neo4j/password + NEO4J_ACCEPT_LICENSE_AGREEMENT: "yes" + ports: + - "7474:7474" + - "7687:7687" + volumes: + - neo4j-data:/data + +volumes: + oracle-data: + neo4j-data: From 8bb32f01d9de3a7e2dac2a2d3138911e4a652caf Mon Sep 17 00:00:00 2001 From: Petr Stepanek Date: Tue, 14 Apr 2026 17:19:07 +0300 Subject: [PATCH 6/7] tests for Oracle config --- cmd/sql-graph-cli/commands/analyze.go | 4 +- .../domain/models/database_config_test.go | 573 ++++++++++++++++++ .../database_repository_factory_test.go | 96 +++ .../persistence/oracle/database_repository.go | 2 +- .../oracle/database_repository_test.go | 494 +++++++++++++++ 5 files changed, 1166 insertions(+), 3 deletions(-) create mode 100644 internal/domain/models/database_config_test.go create mode 100644 internal/infrastructure/factories/database_repository_factory_test.go create mode 100644 internal/infrastructure/persistence/oracle/database_repository_test.go diff --git a/cmd/sql-graph-cli/commands/analyze.go b/cmd/sql-graph-cli/commands/analyze.go index c3a6cb6..8e99c94 100644 --- a/cmd/sql-graph-cli/commands/analyze.go +++ b/cmd/sql-graph-cli/commands/analyze.go @@ -379,7 +379,7 @@ func outputSummary(result *models.UniversalDatabaseAnalysisResult, outputFile st fmt.Fprintf(&output, " Database Type: %s\n", strings.ToUpper(string(result.DatabaseType))) fmt.Fprintf(&output, " Database: %s@%s:%d/%s\n", result.DatabaseInfo.User, result.DatabaseInfo.Host, - result.DatabaseInfo.Port, result.DatabaseInfo.Database) + result.DatabaseInfo.Port, result.DatabaseInfo.Database) fmt.Fprintf(&output, " Server Version: %s\n", result.DatabaseInfo.Version) } fmt.Fprintf(&output, " Processing Time: %v\n", result.ProcessingDuration) @@ -411,7 +411,7 @@ func outputSummary(result *models.UniversalDatabaseAnalysisResult, outputFile st schemaInfo = fmt.Sprintf(" (%s)", table.Schema) } fmt.Fprintf(&output, " %-20s%s - %d rows, %d columns\n", - table.Name, schemaInfo, table.EstimatedRows, len(table.Columns)) + table.Name, schemaInfo, table.EstimatedRows, len(table.Columns)) // Show column details for first few tables if len(result.SchemaAnalysis.Tables) <= 3 && len(table.Columns) > 0 { diff --git a/internal/domain/models/database_config_test.go b/internal/domain/models/database_config_test.go new file mode 100644 index 0000000..1f9540f --- /dev/null +++ b/internal/domain/models/database_config_test.go @@ -0,0 +1,573 @@ +/* + * Copyright (c) 2025 Petr Miroslav Stepanek + * + * This source code is licensed under a Dual License: + * - AGPL-3.0 for open source use (see LICENSE file) + * - Commercial License for business use (contact: petrstepanek99@gmail.com) + * + * This software contains patent-pending innovations in database analysis + * and graph visualization. Commercial use requires separate licensing. + */ + +package models + +import ( + "strings" + "testing" +) + +// --- OracleConfig.Validate() tests --- + +func TestOracleConfig_Validate_ValidMinimal(t *testing.T) { + cfg := &OracleConfig{ + Host: "localhost", + Username: "testuser", + Password: "testpass", + } + + err := cfg.Validate() + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + // Check defaults were set + if cfg.Port != 1521 { + t.Errorf("expected default port 1521, got %d", cfg.Port) + } + if cfg.ServiceName != "XE" { + t.Errorf("expected default service name XE, got %s", cfg.ServiceName) + } + if cfg.MaxOpenConns != 10 { + t.Errorf("expected default MaxOpenConns 10, got %d", cfg.MaxOpenConns) + } + if cfg.MaxIdleConns != 5 { + t.Errorf("expected default MaxIdleConns 5, got %d", cfg.MaxIdleConns) + } + if cfg.ConnectionTimeout != 30 { + t.Errorf("expected default ConnectionTimeout 30, got %d", cfg.ConnectionTimeout) + } + if cfg.QueryTimeout != 30 { + t.Errorf("expected default QueryTimeout 30, got %d", cfg.QueryTimeout) + } + if cfg.ApplicationName != "sql-graph-visualizer" { + t.Errorf("expected default ApplicationName, got %s", cfg.ApplicationName) + } +} + +func TestOracleConfig_Validate_MissingUsername(t *testing.T) { + cfg := &OracleConfig{ + Host: "localhost", + Password: "testpass", + } + + err := cfg.Validate() + if err == nil { + t.Fatal("expected error for missing username") + } + if !strings.Contains(err.Error(), "username is required") { + t.Errorf("unexpected error message: %v", err) + } +} + +func TestOracleConfig_Validate_MissingPassword(t *testing.T) { + cfg := &OracleConfig{ + Host: "localhost", + Username: "testuser", + } + + err := cfg.Validate() + if err == nil { + t.Fatal("expected error for missing password") + } + if !strings.Contains(err.Error(), "password is required") { + t.Errorf("unexpected error message: %v", err) + } +} + +func TestOracleConfig_Validate_MissingHostWithoutDSN(t *testing.T) { + cfg := &OracleConfig{ + Username: "testuser", + Password: "testpass", + } + + err := cfg.Validate() + if err == nil { + t.Fatal("expected error for missing host") + } + if !strings.Contains(err.Error(), "host is required") { + t.Errorf("unexpected error message: %v", err) + } +} + +func TestOracleConfig_Validate_DSNBypassesHostCheck(t *testing.T) { + cfg := &OracleConfig{ + DSN: "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=db.example.com)(PORT=1521))(CONNECT_DATA=(SID=ORCL)))", + Username: "testuser", + Password: "testpass", + } + + err := cfg.Validate() + if err != nil { + t.Fatalf("expected no error with DSN, got: %v", err) + } +} + +func TestOracleConfig_Validate_ExplicitPortPreserved(t *testing.T) { + cfg := &OracleConfig{ + Host: "localhost", + Port: 1522, + Username: "testuser", + Password: "testpass", + } + + err := cfg.Validate() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.Port != 1522 { + t.Errorf("expected port 1522 to be preserved, got %d", cfg.Port) + } +} + +func TestOracleConfig_Validate_ExplicitServiceNamePreserved(t *testing.T) { + cfg := &OracleConfig{ + Host: "localhost", + Username: "testuser", + Password: "testpass", + ServiceName: "ORCL", + } + + err := cfg.Validate() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.ServiceName != "ORCL" { + t.Errorf("expected service name ORCL, got %s", cfg.ServiceName) + } +} + +func TestOracleConfig_Validate_SIDPreventDefaultServiceName(t *testing.T) { + cfg := &OracleConfig{ + Host: "localhost", + Username: "testuser", + Password: "testpass", + SID: "MYSID", + } + + err := cfg.Validate() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // When SID is set, ServiceName should NOT be defaulted to XE + if cfg.ServiceName != "" { + t.Errorf("expected empty service name when SID is set, got %s", cfg.ServiceName) + } +} + +func TestOracleConfig_Validate_SecurityDefaults(t *testing.T) { + cfg := &OracleConfig{ + Host: "localhost", + Username: "testuser", + Password: "testpass", + } + + err := cfg.Validate() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if cfg.Security.MaxConnections != cfg.MaxOpenConns { + t.Errorf("expected Security.MaxConnections=%d, got %d", cfg.MaxOpenConns, cfg.Security.MaxConnections) + } + if cfg.Security.ConnectionTimeout != cfg.ConnectionTimeout { + t.Errorf("expected Security.ConnectionTimeout=%d, got %d", cfg.ConnectionTimeout, cfg.Security.ConnectionTimeout) + } + if cfg.Security.QueryTimeout != cfg.QueryTimeout { + t.Errorf("expected Security.QueryTimeout=%d, got %d", cfg.QueryTimeout, cfg.Security.QueryTimeout) + } +} + +func TestOracleConfig_Validate_ExplicitValuesNotOverwritten(t *testing.T) { + cfg := &OracleConfig{ + Host: "dbhost", + Port: 1523, + ServiceName: "PROD", + Username: "admin", + Password: "secret", + MaxOpenConns: 20, + MaxIdleConns: 10, + ConnectionTimeout: 60, + QueryTimeout: 120, + ApplicationName: "my-app", + Security: SecurityConfig{ + MaxConnections: 15, + ConnectionTimeout: 45, + QueryTimeout: 90, + }, + } + + err := cfg.Validate() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if cfg.MaxOpenConns != 20 { + t.Errorf("MaxOpenConns should not be overwritten, got %d", cfg.MaxOpenConns) + } + if cfg.ApplicationName != "my-app" { + t.Errorf("ApplicationName should not be overwritten, got %s", cfg.ApplicationName) + } + if cfg.Security.MaxConnections != 15 { + t.Errorf("Security.MaxConnections should not be overwritten, got %d", cfg.Security.MaxConnections) + } +} + +// --- OracleConfig.BuildConnectionString() tests --- + +func TestOracleConfig_BuildConnectionString_ServiceName(t *testing.T) { + cfg := &OracleConfig{ + Host: "dbhost", + Port: 1521, + ServiceName: "ORCL", + Username: "admin", + Password: "secret", + } + + connStr := cfg.BuildConnectionString() + expected := "oracle://admin:secret@dbhost:1521/ORCL" + if connStr != expected { + t.Errorf("expected %q, got %q", expected, connStr) + } +} + +func TestOracleConfig_BuildConnectionString_SID(t *testing.T) { + cfg := &OracleConfig{ + Host: "dbhost", + Port: 1521, + SID: "MYSID", + Username: "admin", + Password: "secret", + } + + connStr := cfg.BuildConnectionString() + if !strings.Contains(connStr, "MYSID") { + t.Errorf("expected connection string to contain SID, got %q", connStr) + } + if !strings.Contains(connStr, "SID=MYSID") { + t.Errorf("expected SID parameter in connection string, got %q", connStr) + } +} + +func TestOracleConfig_BuildConnectionString_DSN(t *testing.T) { + cfg := &OracleConfig{ + DSN: "my-tns-descriptor", + Username: "admin", + Password: "secret", + } + + connStr := cfg.BuildConnectionString() + expected := "oracle://admin:secret@my-tns-descriptor" + if connStr != expected { + t.Errorf("expected %q, got %q", expected, connStr) + } +} + +func TestOracleConfig_BuildConnectionString_DefaultXE(t *testing.T) { + cfg := &OracleConfig{ + Host: "dbhost", + Port: 1521, + Username: "admin", + Password: "secret", + } + + connStr := cfg.BuildConnectionString() + expected := "oracle://admin:secret@dbhost:1521/XE" + if connStr != expected { + t.Errorf("expected %q, got %q", expected, connStr) + } +} + +func TestOracleConfig_BuildConnectionString_WithTimeout(t *testing.T) { + cfg := &OracleConfig{ + Host: "dbhost", + Port: 1521, + ServiceName: "ORCL", + Username: "admin", + Password: "secret", + ConnectionTimeout: 60, + } + + connStr := cfg.BuildConnectionString() + if !strings.Contains(connStr, "TIMEOUT=60") { + t.Errorf("expected TIMEOUT parameter, got %q", connStr) + } +} + +func TestOracleConfig_BuildConnectionString_WithTimezone(t *testing.T) { + cfg := &OracleConfig{ + Host: "dbhost", + Port: 1521, + ServiceName: "ORCL", + Username: "admin", + Password: "secret", + Timezone: "UTC", + } + + connStr := cfg.BuildConnectionString() + if !strings.Contains(connStr, "TIMEZONE=UTC") { + t.Errorf("expected TIMEZONE parameter, got %q", connStr) + } +} + +func TestOracleConfig_BuildConnectionString_WithWallet(t *testing.T) { + cfg := &OracleConfig{ + Host: "dbhost", + Port: 1521, + ServiceName: "ORCL", + Username: "admin", + Password: "secret", + WalletLocation: "/opt/wallet", + } + + connStr := cfg.BuildConnectionString() + if !strings.Contains(connStr, "WALLET=/opt/wallet") { + t.Errorf("expected WALLET parameter, got %q", connStr) + } +} + +func TestOracleConfig_BuildConnectionString_MultipleParams(t *testing.T) { + cfg := &OracleConfig{ + Host: "dbhost", + Port: 1521, + ServiceName: "ORCL", + Username: "admin", + Password: "secret", + ConnectionTimeout: 60, + Timezone: "UTC", + } + + connStr := cfg.BuildConnectionString() + if !strings.Contains(connStr, "TIMEOUT=60") { + t.Errorf("expected TIMEOUT parameter, got %q", connStr) + } + if !strings.Contains(connStr, "TIMEZONE=UTC") { + t.Errorf("expected TIMEZONE parameter, got %q", connStr) + } + if !strings.Contains(connStr, "&") { + t.Errorf("expected parameters joined with &, got %q", connStr) + } +} + +func TestOracleConfig_BuildConnectionString_SIDWithParams(t *testing.T) { + cfg := &OracleConfig{ + Host: "dbhost", + Port: 1521, + SID: "MYSID", + Username: "admin", + Password: "secret", + ConnectionTimeout: 30, + } + + connStr := cfg.BuildConnectionString() + // SID URL already contains ?, so params should use & + if !strings.Contains(connStr, "SID=MYSID") { + t.Errorf("expected SID parameter, got %q", connStr) + } + if !strings.Contains(connStr, "TIMEOUT=30") { + t.Errorf("expected TIMEOUT parameter, got %q", connStr) + } + if !strings.Contains(connStr, "&TIMEOUT") { + t.Errorf("expected & separator for additional params with SID, got %q", connStr) + } +} + +// --- DatabaseConfig Oracle methods tests --- + +func TestDatabaseConfig_GetHost_Oracle(t *testing.T) { + cfg := DatabaseConfig{ + Type: DatabaseTypeOracle, + Oracle: &OracleConfig{Host: "oracle-host"}, + } + + if cfg.GetHost() != "oracle-host" { + t.Errorf("expected oracle-host, got %s", cfg.GetHost()) + } +} + +func TestDatabaseConfig_GetHost_Oracle_Nil(t *testing.T) { + cfg := DatabaseConfig{ + Type: DatabaseTypeOracle, + } + + if cfg.GetHost() != "" { + t.Errorf("expected empty string for nil Oracle config, got %s", cfg.GetHost()) + } +} + +func TestDatabaseConfig_GetPort_Oracle(t *testing.T) { + cfg := DatabaseConfig{ + Type: DatabaseTypeOracle, + Oracle: &OracleConfig{Port: 1521}, + } + + if cfg.GetPort() != 1521 { + t.Errorf("expected 1521, got %d", cfg.GetPort()) + } +} + +func TestDatabaseConfig_GetPort_Oracle_Nil(t *testing.T) { + cfg := DatabaseConfig{ + Type: DatabaseTypeOracle, + } + + if cfg.GetPort() != 0 { + t.Errorf("expected 0 for nil Oracle config, got %d", cfg.GetPort()) + } +} + +func TestDatabaseConfig_GetUsername_Oracle(t *testing.T) { + cfg := DatabaseConfig{ + Type: DatabaseTypeOracle, + Oracle: &OracleConfig{Username: "orauser"}, + } + + if cfg.GetUsername() != "orauser" { + t.Errorf("expected orauser, got %s", cfg.GetUsername()) + } +} + +func TestDatabaseConfig_GetUsername_Oracle_Nil(t *testing.T) { + cfg := DatabaseConfig{ + Type: DatabaseTypeOracle, + } + + if cfg.GetUsername() != "" { + t.Errorf("expected empty string for nil Oracle config, got %s", cfg.GetUsername()) + } +} + +func TestDatabaseConfig_GetEffectiveConfig_Oracle(t *testing.T) { + oracleCfg := &OracleConfig{Host: "oracle-host"} + cfg := DatabaseConfig{ + Type: DatabaseTypeOracle, + Oracle: oracleCfg, + } + + result := cfg.GetEffectiveConfig() + if result != oracleCfg { + t.Error("expected GetEffectiveConfig to return Oracle config") + } +} + +func TestDatabaseConfig_GetEffectiveConfig_Unsupported(t *testing.T) { + cfg := DatabaseConfig{ + Type: "unknown", + } + + if cfg.GetEffectiveConfig() != nil { + t.Error("expected nil for unsupported type") + } +} + +func TestDatabaseConfig_GetDatabaseType_Oracle(t *testing.T) { + cfg := DatabaseConfig{Type: DatabaseTypeOracle} + if cfg.GetDatabaseType() != DatabaseTypeOracle { + t.Errorf("expected oracle, got %s", cfg.GetDatabaseType()) + } +} + +// --- DatabaseConfig.Validate() tests --- + +func TestDatabaseConfig_Validate_Oracle_Valid(t *testing.T) { + cfg := DatabaseConfig{ + Type: DatabaseTypeOracle, + Oracle: &OracleConfig{ + Host: "localhost", + Username: "admin", + Password: "secret", + }, + } + + err := cfg.Validate() + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } +} + +func TestDatabaseConfig_Validate_Oracle_NilConfig(t *testing.T) { + cfg := DatabaseConfig{ + Type: DatabaseTypeOracle, + } + + err := cfg.Validate() + if err == nil { + t.Fatal("expected error for nil Oracle config") + } + if !strings.Contains(err.Error(), "oracle configuration is required") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestDatabaseConfig_Validate_UnsupportedType(t *testing.T) { + cfg := DatabaseConfig{ + Type: "sqlite", + } + + err := cfg.Validate() + if err == nil { + t.Fatal("expected error for unsupported type") + } + if !strings.Contains(err.Error(), "unsupported database type") { + t.Errorf("unexpected error: %v", err) + } +} + +// --- DatabaseSelector.GetActiveConfig() tests --- + +func TestDatabaseSelector_GetActiveConfig_Oracle(t *testing.T) { + oracleCfg := &OracleConfig{Host: "oracle-host", Username: "orauser"} + selector := &DatabaseSelector{ + Type: DatabaseTypeOracle, + OracleConfig: oracleCfg, + } + + config := selector.GetActiveConfig() + if config.Type != DatabaseTypeOracle { + t.Errorf("expected oracle type, got %s", config.Type) + } + if config.Oracle != oracleCfg { + t.Error("expected Oracle config to match") + } +} + +func TestDatabaseSelector_GetActiveConfig_Oracle_Nil(t *testing.T) { + selector := &DatabaseSelector{ + Type: DatabaseTypeOracle, + } + + config := selector.GetActiveConfig() + if config.Type != "" { + t.Errorf("expected empty config for nil Oracle, got type %s", config.Type) + } +} + +// --- DatabaseType tests --- + +func TestDatabaseType_IsSupported_Oracle(t *testing.T) { + if !DatabaseTypeOracle.IsSupported() { + t.Error("Oracle should be supported") + } +} + +func TestDatabaseType_IsSupported_Unknown(t *testing.T) { + dt := DatabaseType("sqlite") + if dt.IsSupported() { + t.Error("sqlite should not be supported") + } +} + +func TestDatabaseType_String_Oracle(t *testing.T) { + if DatabaseTypeOracle.String() != "oracle" { + t.Errorf("expected 'oracle', got %s", DatabaseTypeOracle.String()) + } +} diff --git a/internal/infrastructure/factories/database_repository_factory_test.go b/internal/infrastructure/factories/database_repository_factory_test.go new file mode 100644 index 0000000..d6df169 --- /dev/null +++ b/internal/infrastructure/factories/database_repository_factory_test.go @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025 Petr Miroslav Stepanek + * + * This source code is licensed under a Dual License: + * - AGPL-3.0 for open source use (see LICENSE file) + * - Commercial License for business use (contact: petrstepanek99@gmail.com) + * + * This software contains patent-pending innovations in database analysis + * and graph visualization. Commercial use requires separate licensing. + */ + +package factories + +import ( + "testing" + + "sql-graph-visualizer/internal/domain/models" + "sql-graph-visualizer/internal/infrastructure/persistence/oracle" +) + +func TestCreateRepository_Oracle(t *testing.T) { + factory := NewDatabaseRepositoryFactory() + repo, err := factory.CreateRepository(models.DatabaseTypeOracle) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if repo == nil { + t.Fatal("expected non-nil repository") + } + + _, ok := repo.(*oracle.OracleDatabaseRepository) + if !ok { + t.Errorf("expected *oracle.OracleDatabaseRepository, got %T", repo) + } +} + +func TestCreateRepository_MySQL(t *testing.T) { + factory := NewDatabaseRepositoryFactory() + repo, err := factory.CreateRepository(models.DatabaseTypeMySQL) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if repo == nil { + t.Fatal("expected non-nil repository") + } +} + +func TestCreateRepository_PostgreSQL(t *testing.T) { + factory := NewDatabaseRepositoryFactory() + repo, err := factory.CreateRepository(models.DatabaseTypePostgreSQL) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + if repo == nil { + t.Fatal("expected non-nil repository") + } +} + +func TestCreateRepository_Unsupported(t *testing.T) { + factory := NewDatabaseRepositoryFactory() + _, err := factory.CreateRepository("sqlite") + if err == nil { + t.Fatal("expected error for unsupported type") + } +} + +func TestGetSupportedDatabaseTypes(t *testing.T) { + factory := NewDatabaseRepositoryFactory() + types := factory.GetSupportedDatabaseTypes() + + if len(types) != 3 { + t.Fatalf("expected 3 supported types, got %d", len(types)) + } + + typeMap := make(map[models.DatabaseType]bool) + for _, dt := range types { + typeMap[dt] = true + } + + if !typeMap[models.DatabaseTypeMySQL] { + t.Error("MySQL should be in supported types") + } + if !typeMap[models.DatabaseTypePostgreSQL] { + t.Error("PostgreSQL should be in supported types") + } + if !typeMap[models.DatabaseTypeOracle] { + t.Error("Oracle should be in supported types") + } +} + +func TestNewDatabaseRepositoryFactory(t *testing.T) { + factory := NewDatabaseRepositoryFactory() + if factory == nil { + t.Fatal("expected non-nil factory") + } +} diff --git a/internal/infrastructure/persistence/oracle/database_repository.go b/internal/infrastructure/persistence/oracle/database_repository.go index 4080497..32b46bf 100644 --- a/internal/infrastructure/persistence/oracle/database_repository.go +++ b/internal/infrastructure/persistence/oracle/database_repository.go @@ -472,7 +472,7 @@ func (r *OracleDatabaseRepository) GetDatabaseType() models.DatabaseType { // GetConnectionString builds Oracle connection string from config. func (r *OracleDatabaseRepository) GetConnectionString(config models.DatabaseConfig) string { oracleConfig, ok := config.GetEffectiveConfig().(*models.OracleConfig) - if !ok { + if !ok || oracleConfig == nil { return "" } return oracleConfig.BuildConnectionString() diff --git a/internal/infrastructure/persistence/oracle/database_repository_test.go b/internal/infrastructure/persistence/oracle/database_repository_test.go new file mode 100644 index 0000000..3067b29 --- /dev/null +++ b/internal/infrastructure/persistence/oracle/database_repository_test.go @@ -0,0 +1,494 @@ +/* + * Copyright (c) 2025 Petr Miroslav Stepanek + * + * This source code is licensed under a Dual License: + * - AGPL-3.0 for open source use (see LICENSE file) + * - Commercial License for business use (contact: petrstepanek99@gmail.com) + * + * This software contains patent-pending innovations in database analysis + * and graph visualization. Commercial use requires separate licensing. + */ + +package oracle + +import ( + "context" + "strings" + "testing" + + "sql-graph-visualizer/internal/domain/models" +) + +// --- EscapeIdentifier tests --- + +func TestEscapeIdentifier_Simple(t *testing.T) { + repo := &OracleDatabaseRepository{} + result := repo.EscapeIdentifier("EMPLOYEES") + expected := `"EMPLOYEES"` + if result != expected { + t.Errorf("expected %q, got %q", expected, result) + } +} + +func TestEscapeIdentifier_WithQuotes(t *testing.T) { + repo := &OracleDatabaseRepository{} + // Double quotes inside should be escaped by doubling + result := repo.EscapeIdentifier(`my"table`) + expected := `"my""table"` + if result != expected { + t.Errorf("expected %q, got %q", expected, result) + } +} + +func TestEscapeIdentifier_Empty(t *testing.T) { + repo := &OracleDatabaseRepository{} + result := repo.EscapeIdentifier("") + expected := `""` + if result != expected { + t.Errorf("expected %q, got %q", expected, result) + } +} + +func TestEscapeIdentifier_MultipleQuotes(t *testing.T) { + repo := &OracleDatabaseRepository{} + result := repo.EscapeIdentifier(`a"b"c`) + expected := `"a""b""c"` + if result != expected { + t.Errorf("expected %q, got %q", expected, result) + } +} + +// --- GetQuoteChar tests --- + +func TestGetQuoteChar(t *testing.T) { + repo := &OracleDatabaseRepository{} + if repo.GetQuoteChar() != `"` { + t.Errorf("expected double quote, got %s", repo.GetQuoteChar()) + } +} + +// --- GetDatabaseType tests --- + +func TestGetDatabaseType(t *testing.T) { + repo := &OracleDatabaseRepository{} + if repo.GetDatabaseType() != models.DatabaseTypeOracle { + t.Errorf("expected oracle, got %s", repo.GetDatabaseType()) + } +} + +// --- TestConnection without active connection --- + +func TestTestConnection_NoActiveConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + err := repo.TestConnection(context.Background()) + if err == nil { + t.Fatal("expected error for no active connection") + } + if !strings.Contains(err.Error(), "no active database connection") { + t.Errorf("unexpected error: %v", err) + } +} + +// --- Close without active connection --- + +func TestClose_NoActiveConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + err := repo.Close() + if err != nil { + t.Errorf("Close on nil db should not error, got: %v", err) + } +} + +// --- Methods that require connection return error when db is nil --- + +func TestGetTables_NoConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetTables(context.Background(), models.DataFilteringConfig{}) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "no active database connection") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestGetColumns_NoConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetColumns(context.Background(), "test_table") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "no active database connection") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestGetForeignKeys_NoConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetForeignKeys(context.Background(), "test_table") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "no active database connection") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestGetIndexes_NoConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetIndexes(context.Background(), "test_table") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "no active database connection") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestGetConstraints_NoConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetConstraints(context.Background(), "test_table") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "no active database connection") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestGetDatabaseName_NoConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetDatabaseName(context.Background()) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "no active database connection") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestGetDatabaseVersion_NoConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetDatabaseVersion(context.Background()) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "no active database connection") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestGetSchemaNames_NoConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetSchemaNames(context.Background()) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "no active database connection") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestGetTableRowCount_NoConnection(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetTableRowCount(context.Background(), "test_table") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "no active database connection") { + t.Errorf("unexpected error: %v", err) + } +} + +// --- Not-implemented stubs --- + +func TestSampleTableData_NotImplemented(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.SampleTableData(context.Background(), "test", 10) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "not implemented") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestAnalyzeColumnStatistics_NotImplemented(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.AnalyzeColumnStatistics(context.Background(), "test", "col") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "not implemented") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestGetTableSize_NotImplemented(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetTableSize(context.Background(), "test") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "not implemented") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestGetQueryExecutionPlan_NotImplemented(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.GetQueryExecutionPlan(context.Background(), "SELECT 1") + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "not implemented") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestValidatePermissions_NotImplemented(t *testing.T) { + repo := &OracleDatabaseRepository{} + err := repo.ValidatePermissions(context.Background(), []string{"SELECT"}) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "not implemented") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestCheckUserPrivileges_NotImplemented(t *testing.T) { + repo := &OracleDatabaseRepository{} + _, err := repo.CheckUserPrivileges(context.Background()) + if err == nil { + t.Fatal("expected error") + } + if !strings.Contains(err.Error(), "not implemented") { + t.Errorf("unexpected error: %v", err) + } +} + +// --- GetConnectionString tests --- + +func TestGetConnectionString_ValidOracleConfig(t *testing.T) { + repo := &OracleDatabaseRepository{} + cfg := models.DatabaseConfig{ + Type: models.DatabaseTypeOracle, + Oracle: &models.OracleConfig{ + Host: "dbhost", + Port: 1521, + ServiceName: "ORCL", + Username: "admin", + Password: "secret", + }, + } + + connStr := repo.GetConnectionString(cfg) + if !strings.Contains(connStr, "oracle://") { + t.Errorf("expected oracle:// prefix, got %q", connStr) + } + if !strings.Contains(connStr, "dbhost:1521/ORCL") { + t.Errorf("expected host:port/service in conn string, got %q", connStr) + } +} + +func TestGetConnectionString_WrongConfigType(t *testing.T) { + repo := &OracleDatabaseRepository{} + cfg := models.DatabaseConfig{ + Type: models.DatabaseTypeMySQL, + MySQL: &models.MySQLConfig{ + Host: "localhost", + }, + } + + connStr := repo.GetConnectionString(cfg) + if connStr != "" { + t.Errorf("expected empty string for non-Oracle config, got %q", connStr) + } +} + +func TestGetConnectionString_NilOracleConfig(t *testing.T) { + repo := &OracleDatabaseRepository{} + cfg := models.DatabaseConfig{ + Type: models.DatabaseTypeOracle, + } + + connStr := repo.GetConnectionString(cfg) + if connStr != "" { + t.Errorf("expected empty string for nil Oracle config, got %q", connStr) + } +} + +// --- NewOracleDatabaseRepository tests --- + +func TestNewOracleDatabaseRepository(t *testing.T) { + repo := NewOracleDatabaseRepository() + if repo == nil { + t.Fatal("expected non-nil repository") + } + + oracleRepo, ok := repo.(*OracleDatabaseRepository) + if !ok { + t.Fatal("expected *OracleDatabaseRepository type") + } + if oracleRepo.db != nil { + t.Error("expected nil db on new repository") + } +} + +// --- applyTableFiltering tests --- + +func TestApplyTableFiltering_NoFilters(t *testing.T) { + tables := []string{"EMPLOYEES", "DEPARTMENTS", "JOBS"} + filters := models.DataFilteringConfig{} + + result := applyTableFiltering(tables, filters) + if len(result) != 3 { + t.Errorf("expected 3 tables, got %d", len(result)) + } +} + +func TestApplyTableFiltering_Whitelist(t *testing.T) { + tables := []string{"EMPLOYEES", "DEPARTMENTS", "JOBS", "LOCATIONS"} + filters := models.DataFilteringConfig{ + TableWhitelist: []string{"EMPLOYEES", "JOBS"}, + } + + result := applyTableFiltering(tables, filters) + if len(result) != 2 { + t.Errorf("expected 2 tables, got %d", len(result)) + } + for _, table := range result { + if table != "EMPLOYEES" && table != "JOBS" { + t.Errorf("unexpected table in result: %s", table) + } + } +} + +func TestApplyTableFiltering_Blacklist(t *testing.T) { + tables := []string{"EMPLOYEES", "DEPARTMENTS", "JOBS", "LOCATIONS"} + filters := models.DataFilteringConfig{ + TableBlacklist: []string{"DEPARTMENTS", "LOCATIONS"}, + } + + result := applyTableFiltering(tables, filters) + if len(result) != 2 { + t.Errorf("expected 2 tables, got %d", len(result)) + } + for _, table := range result { + if table == "DEPARTMENTS" || table == "LOCATIONS" { + t.Errorf("blacklisted table should not be in result: %s", table) + } + } +} + +func TestApplyTableFiltering_BlacklistTakesPrecedence(t *testing.T) { + tables := []string{"EMPLOYEES", "DEPARTMENTS", "JOBS"} + filters := models.DataFilteringConfig{ + TableWhitelist: []string{"EMPLOYEES", "DEPARTMENTS"}, + TableBlacklist: []string{"DEPARTMENTS"}, + } + + result := applyTableFiltering(tables, filters) + if len(result) != 1 { + t.Errorf("expected 1 table, got %d", len(result)) + } + if len(result) > 0 && result[0] != "EMPLOYEES" { + t.Errorf("expected EMPLOYEES, got %s", result[0]) + } +} + +func TestApplyTableFiltering_CaseInsensitive(t *testing.T) { + tables := []string{"Employees", "departments"} + filters := models.DataFilteringConfig{ + TableWhitelist: []string{"EMPLOYEES", "DEPARTMENTS"}, + } + + result := applyTableFiltering(tables, filters) + if len(result) != 2 { + t.Errorf("expected 2 tables (case insensitive), got %d", len(result)) + } +} + +func TestApplyTableFiltering_EmptyTables(t *testing.T) { + tables := []string{} + filters := models.DataFilteringConfig{ + TableWhitelist: []string{"EMPLOYEES"}, + } + + result := applyTableFiltering(tables, filters) + if len(result) != 0 { + t.Errorf("expected 0 tables, got %d", len(result)) + } +} + +// --- isInList tests --- + +func TestIsInList_Found(t *testing.T) { + if !isInList("EMPLOYEES", []string{"EMPLOYEES", "DEPARTMENTS"}) { + t.Error("expected EMPLOYEES to be in list") + } +} + +func TestIsInList_NotFound(t *testing.T) { + if isInList("JOBS", []string{"EMPLOYEES", "DEPARTMENTS"}) { + t.Error("expected JOBS not to be in list") + } +} + +func TestIsInList_CaseInsensitive(t *testing.T) { + if !isInList("employees", []string{"EMPLOYEES", "DEPARTMENTS"}) { + t.Error("expected case-insensitive match") + } +} + +func TestIsInList_EmptyList(t *testing.T) { + if isInList("EMPLOYEES", []string{}) { + t.Error("expected false for empty list") + } +} + +func TestIsInList_EmptyItem(t *testing.T) { + if isInList("", []string{"EMPLOYEES"}) { + t.Error("expected false for empty item") + } +} + +// --- Connect with invalid config tests --- + +func TestConnect_InvalidConfigType(t *testing.T) { + repo := &OracleDatabaseRepository{} + cfg := models.DatabaseConfig{ + Type: models.DatabaseTypeMySQL, + MySQL: &models.MySQLConfig{ + Host: "localhost", + }, + } + + _, err := repo.Connect(context.Background(), cfg) + if err == nil { + t.Fatal("expected error for non-Oracle config") + } + if !strings.Contains(err.Error(), "expected OracleConfig") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestConnect_InvalidOracleConfig(t *testing.T) { + repo := &OracleDatabaseRepository{} + cfg := models.DatabaseConfig{ + Type: models.DatabaseTypeOracle, + Oracle: &models.OracleConfig{ + // Missing required fields + Host: "localhost", + }, + } + + _, err := repo.Connect(context.Background(), cfg) + if err == nil { + t.Fatal("expected error for invalid Oracle config") + } + if !strings.Contains(err.Error(), "invalid Oracle configuration") { + t.Errorf("unexpected error: %v", err) + } +} From d73fb8105e4ebb51ac20f681c2eea7cb58c0dbe3 Mon Sep 17 00:00:00 2001 From: Petr Stepanek Date: Wed, 15 Apr 2026 14:33:42 +0300 Subject: [PATCH 7/7] dependecies update --- go.mod | 46 +++++++++++++--------------- go.sum | 94 ++++++++++++++++++++++++++++------------------------------ 2 files changed, 66 insertions(+), 74 deletions(-) diff --git a/go.mod b/go.mod index eec75a5..3e76b2a 100644 --- a/go.mod +++ b/go.mod @@ -1,47 +1,43 @@ module sql-graph-visualizer -go 1.24.0 - -toolchain go1.24.6 +go 1.26.2 require ( - github.com/99designs/gqlgen v0.17.79 - github.com/go-sql-driver/mysql v1.8.1 + github.com/99designs/gqlgen v0.17.89 + github.com/go-sql-driver/mysql v1.9.3 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 - github.com/gorilla/websocket v1.5.0 - github.com/lib/pq v1.10.9 - github.com/neo4j/neo4j-go-driver/v4 v4.4.7 + github.com/gorilla/websocket v1.5.3 + github.com/lib/pq v1.12.3 + github.com/neo4j/neo4j-go-driver/v4 v4.4.8 github.com/rs/cors v1.11.1 github.com/sijms/go-ora/v2 v2.9.0 - github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.10.1 + github.com/sirupsen/logrus v1.9.4 + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 - github.com/vektah/gqlparser/v2 v2.5.30 - golang.org/x/text v0.31.0 + github.com/vektah/gqlparser/v2 v2.5.32 + golang.org/x/text v0.36.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/edwards25519 v1.2.0 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sosodev/duration v1.3.1 // indirect - github.com/spf13/pflag v1.0.9 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/urfave/cli/v2 v2.27.7 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/tools v0.38.0 // indirect + github.com/sosodev/duration v1.4.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/urfave/cli/v3 v3.8.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/tools v0.44.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index 74c3d64..a775b83 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/99designs/gqlgen v0.17.79 h1:RTsJZtdzcfROeWdt42NGMIabIbiBn69YyVmLEAuxtnA= -github.com/99designs/gqlgen v0.17.79/go.mod h1:vgNcZlLwemsUhYim4dC1pvFP5FX0pr2Y+uYUoHFb1ig= -github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= -github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/99designs/gqlgen v0.17.89 h1:KzEcxPiMgQoMw3m/E85atUEHyZyt0PbAflMia5Kw8z8= +github.com/99designs/gqlgen v0.17.89/go.mod h1:GFqruTVGB7ZTdrf1uzOagpXbY7DrEt1pIxnTdhIbWvQ= +github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= +github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= @@ -13,8 +13,6 @@ github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmg github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= -github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -25,11 +23,13 @@ github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= -github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -49,8 +49,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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -60,10 +60,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/neo4j/neo4j-go-driver/v4 v4.4.7 h1:6D0DPI7VOVF6zB8eubY1lav7RI7dZ2mytnr3fj369Ow= -github.com/neo4j/neo4j-go-driver/v4 v4.4.7/go.mod h1:NexOfrm4c317FVjekrhVV8pHBXgtMG5P6GeweJWCyo4= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/neo4j/neo4j-go-driver/v4 v4.4.8 h1:Gc+5w6jgVs1E2LoluUHDsV9I5sysJlsV9FXtd8czQjg= +github.com/neo4j/neo4j-go-driver/v4 v4.4.8/go.mod h1:NexOfrm4c317FVjekrhVV8pHBXgtMG5P6GeweJWCyo4= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -81,40 +81,38 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sijms/go-ora/v2 v2.9.0 h1:+iQbUeTeCOFMb5BsOMgUhV8KWyrv9yjKpcK4x7+MFrg= github.com/sijms/go-ora/v2 v2.9.0/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYoCqhR2dU= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= -github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= +github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= -github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE= -github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= +github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= +github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -122,13 +120,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -142,20 +140,19 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -178,6 +175,5 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=