mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 15:36:48 +00:00
feat(sqlschema): add sqlschema (#8384)
## 📄 Summary
- add sqlschema package
- add unique index on email,org_id in users and user_invite
This commit is contained in:
parent
06ef9ff384
commit
c9e48b6de9
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/ee/licensing"
|
"github.com/SigNoz/signoz/ee/licensing"
|
||||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/app"
|
"github.com/SigNoz/signoz/ee/query-service/app"
|
||||||
|
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
|
||||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||||
"github.com/SigNoz/signoz/ee/zeus"
|
"github.com/SigNoz/signoz/ee/zeus"
|
||||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||||
@ -21,6 +22,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
@ -145,6 +147,14 @@ func main() {
|
|||||||
signoz.NewEmailingProviderFactories(),
|
signoz.NewEmailingProviderFactories(),
|
||||||
signoz.NewCacheProviderFactories(),
|
signoz.NewCacheProviderFactories(),
|
||||||
signoz.NewWebProviderFactories(),
|
signoz.NewWebProviderFactories(),
|
||||||
|
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
|
||||||
|
existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
|
||||||
|
if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {
|
||||||
|
zap.L().Fatal("Failed to add postgressqlschema factory", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingFactories
|
||||||
|
},
|
||||||
sqlStoreFactories,
|
sqlStoreFactories,
|
||||||
signoz.NewTelemetryStoreProviderFactories(),
|
signoz.NewTelemetryStoreProviderFactories(),
|
||||||
)
|
)
|
||||||
|
|||||||
36
ee/sqlschema/postgressqlschema/formatter.go
Normal file
36
ee/sqlschema/postgressqlschema/formatter.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package postgressqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Formatter struct {
|
||||||
|
sqlschema.Formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter Formatter) SQLDataTypeOf(dataType sqlschema.DataType) string {
|
||||||
|
if dataType == sqlschema.DataTypeTimestamp {
|
||||||
|
return "TIMESTAMPTZ"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToUpper(dataType.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter Formatter) DataTypeOf(dataType string) sqlschema.DataType {
|
||||||
|
switch strings.ToUpper(dataType) {
|
||||||
|
case "TIMESTAMPTZ", "TIMESTAMP", "TIMESTAMP WITHOUT TIME ZONE", "TIMESTAMP WITH TIME ZONE":
|
||||||
|
return sqlschema.DataTypeTimestamp
|
||||||
|
case "INT8":
|
||||||
|
return sqlschema.DataTypeBigInt
|
||||||
|
case "INT2", "INT4", "SMALLINT", "INTEGER":
|
||||||
|
return sqlschema.DataTypeInteger
|
||||||
|
case "BOOL", "BOOLEAN":
|
||||||
|
return sqlschema.DataTypeBoolean
|
||||||
|
case "VARCHAR", "CHARACTER VARYING", "CHARACTER":
|
||||||
|
return sqlschema.DataTypeText
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter.Formatter.DataTypeOf(dataType)
|
||||||
|
}
|
||||||
285
ee/sqlschema/postgressqlschema/provider.go
Normal file
285
ee/sqlschema/postgressqlschema/provider.go
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
package postgressqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
settings factory.ScopedProviderSettings
|
||||||
|
fmter sqlschema.SQLFormatter
|
||||||
|
sqlstore sqlstore.SQLStore
|
||||||
|
operator sqlschema.SQLOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("postgres"), func(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config) (sqlschema.SQLSchema, error) {
|
||||||
|
return New(ctx, providerSettings, config, sqlstore)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config, sqlstore sqlstore.SQLStore) (sqlschema.SQLSchema, error) {
|
||||||
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/sqlschema/postgressqlschema")
|
||||||
|
fmter := Formatter{Formatter: sqlschema.NewFormatter(sqlstore.BunDB().Dialect())}
|
||||||
|
|
||||||
|
return &provider{
|
||||||
|
sqlstore: sqlstore,
|
||||||
|
fmter: fmter,
|
||||||
|
settings: settings,
|
||||||
|
operator: sqlschema.NewOperator(fmter, sqlschema.OperatorSupport{
|
||||||
|
DropConstraint: true,
|
||||||
|
ColumnIfNotExistsExists: true,
|
||||||
|
AlterColumnSetNotNull: true,
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Formatter() sqlschema.SQLFormatter {
|
||||||
|
return provider.fmter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Operator() sqlschema.SQLOperator {
|
||||||
|
return provider.operator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetTable(ctx context.Context, tableName sqlschema.TableName) (*sqlschema.Table, []*sqlschema.UniqueConstraint, error) {
|
||||||
|
rows, err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
c.column_name,
|
||||||
|
c.is_nullable = 'YES',
|
||||||
|
c.udt_name,
|
||||||
|
c.column_default
|
||||||
|
FROM
|
||||||
|
information_schema.columns AS c
|
||||||
|
WHERE
|
||||||
|
c.table_name = ?`, string(tableName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
columns := make([]*sqlschema.Column, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
sqlDataType string
|
||||||
|
nullable bool
|
||||||
|
defaultVal *string
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&name, &nullable, &sqlDataType, &defaultVal); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
columnDefault := ""
|
||||||
|
if defaultVal != nil {
|
||||||
|
columnDefault = *defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
|
columns = append(columns, &sqlschema.Column{
|
||||||
|
Name: sqlschema.ColumnName(name),
|
||||||
|
Nullable: nullable,
|
||||||
|
DataType: provider.fmter.DataTypeOf(sqlDataType),
|
||||||
|
Default: columnDefault,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
constraintsRows, err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
c.column_name,
|
||||||
|
constraint_name,
|
||||||
|
constraint_type
|
||||||
|
FROM
|
||||||
|
information_schema.table_constraints tc
|
||||||
|
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_catalog, table_name, constraint_name)
|
||||||
|
JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
|
||||||
|
WHERE
|
||||||
|
c.table_name = ?`, string(tableName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := constraintsRows.Close(); err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var primaryKeyConstraint *sqlschema.PrimaryKeyConstraint
|
||||||
|
uniqueConstraintsMap := make(map[string]*sqlschema.UniqueConstraint)
|
||||||
|
for constraintsRows.Next() {
|
||||||
|
var (
|
||||||
|
name string
|
||||||
|
constraintName string
|
||||||
|
constraintType string
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := constraintsRows.Scan(&name, &constraintName, &constraintType); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if constraintType == "PRIMARY KEY" {
|
||||||
|
if primaryKeyConstraint == nil {
|
||||||
|
primaryKeyConstraint = (&sqlschema.PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(name)},
|
||||||
|
}).Named(constraintName).(*sqlschema.PrimaryKeyConstraint)
|
||||||
|
} else {
|
||||||
|
primaryKeyConstraint.ColumnNames = append(primaryKeyConstraint.ColumnNames, sqlschema.ColumnName(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if constraintType == "UNIQUE" {
|
||||||
|
if _, ok := uniqueConstraintsMap[constraintName]; !ok {
|
||||||
|
uniqueConstraintsMap[constraintName] = (&sqlschema.UniqueConstraint{
|
||||||
|
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(name)},
|
||||||
|
}).Named(constraintName).(*sqlschema.UniqueConstraint)
|
||||||
|
} else {
|
||||||
|
uniqueConstraintsMap[constraintName].ColumnNames = append(uniqueConstraintsMap[constraintName].ColumnNames, sqlschema.ColumnName(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreignKeyConstraintsRows, err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
tc.constraint_name,
|
||||||
|
kcu.table_name AS referencing_table,
|
||||||
|
kcu.column_name AS referencing_column,
|
||||||
|
ccu.table_name AS referenced_table,
|
||||||
|
ccu.column_name AS referenced_column
|
||||||
|
FROM
|
||||||
|
information_schema.key_column_usage kcu
|
||||||
|
JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
|
||||||
|
JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
|
||||||
|
WHERE
|
||||||
|
tc.constraint_type = ?
|
||||||
|
AND kcu.table_name = ?`, "FOREIGN KEY", string(tableName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := foreignKeyConstraintsRows.Close(); err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
foreignKeyConstraints := make([]*sqlschema.ForeignKeyConstraint, 0)
|
||||||
|
for foreignKeyConstraintsRows.Next() {
|
||||||
|
var (
|
||||||
|
constraintName string
|
||||||
|
referencingTable string
|
||||||
|
referencingColumn string
|
||||||
|
referencedTable string
|
||||||
|
referencedColumn string
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := foreignKeyConstraintsRows.Scan(&constraintName, &referencingTable, &referencingColumn, &referencedTable, &referencedColumn); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
foreignKeyConstraints = append(foreignKeyConstraints, (&sqlschema.ForeignKeyConstraint{
|
||||||
|
ReferencingColumnName: sqlschema.ColumnName(referencingColumn),
|
||||||
|
ReferencedTableName: sqlschema.TableName(referencedTable),
|
||||||
|
ReferencedColumnName: sqlschema.ColumnName(referencedColumn),
|
||||||
|
}).Named(constraintName).(*sqlschema.ForeignKeyConstraint))
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueConstraints := make([]*sqlschema.UniqueConstraint, 0)
|
||||||
|
for _, uniqueConstraint := range uniqueConstraintsMap {
|
||||||
|
uniqueConstraints = append(uniqueConstraints, uniqueConstraint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sqlschema.Table{
|
||||||
|
Name: tableName,
|
||||||
|
Columns: columns,
|
||||||
|
PrimaryKeyConstraint: primaryKeyConstraint,
|
||||||
|
ForeignKeyConstraints: foreignKeyConstraints,
|
||||||
|
}, uniqueConstraints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetIndices(ctx context.Context, name sqlschema.TableName) ([]sqlschema.Index, error) {
|
||||||
|
rows, err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
ct.relname AS table_name,
|
||||||
|
ci.relname AS index_name,
|
||||||
|
i.indisunique AS unique,
|
||||||
|
i.indisprimary AS primary,
|
||||||
|
a.attname AS column_name
|
||||||
|
FROM
|
||||||
|
pg_index i
|
||||||
|
LEFT JOIN pg_class ct ON ct.oid = i.indrelid
|
||||||
|
LEFT JOIN pg_class ci ON ci.oid = i.indexrelid
|
||||||
|
LEFT JOIN pg_attribute a ON a.attrelid = ct.oid
|
||||||
|
LEFT JOIN pg_constraint con ON con.conindid = i.indexrelid
|
||||||
|
WHERE
|
||||||
|
a.attnum = ANY(i.indkey)
|
||||||
|
AND con.oid IS NULL
|
||||||
|
AND ct.relkind = 'r'
|
||||||
|
AND ct.relname = ?`, string(name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
uniqueIndicesMap := make(map[string]*sqlschema.UniqueIndex)
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
tableName string
|
||||||
|
indexName string
|
||||||
|
unique bool
|
||||||
|
primary bool
|
||||||
|
columnName string
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := rows.Scan(&tableName, &indexName, &unique, &primary, &columnName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if unique {
|
||||||
|
if _, ok := uniqueIndicesMap[indexName]; !ok {
|
||||||
|
uniqueIndicesMap[indexName] = &sqlschema.UniqueIndex{
|
||||||
|
TableName: name,
|
||||||
|
ColumnNames: []sqlschema.ColumnName{sqlschema.ColumnName(columnName)},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uniqueIndicesMap[indexName].ColumnNames = append(uniqueIndicesMap[indexName].ColumnNames, sqlschema.ColumnName(columnName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indices := make([]sqlschema.Index, 0)
|
||||||
|
for _, index := range uniqueIndicesMap {
|
||||||
|
indices = append(indices, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return indices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) ToggleFKEnforcement(_ context.Context, _ bun.IDB, _ bool) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/query-service/app"
|
"github.com/SigNoz/signoz/pkg/query-service/app"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/version"
|
"github.com/SigNoz/signoz/pkg/version"
|
||||||
@ -137,6 +138,9 @@ func main() {
|
|||||||
signoz.NewEmailingProviderFactories(),
|
signoz.NewEmailingProviderFactories(),
|
||||||
signoz.NewCacheProviderFactories(),
|
signoz.NewCacheProviderFactories(),
|
||||||
signoz.NewWebProviderFactories(),
|
signoz.NewWebProviderFactories(),
|
||||||
|
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
|
||||||
|
return signoz.NewSQLSchemaProviderFactories(sqlstore)
|
||||||
|
},
|
||||||
signoz.NewSQLStoreProviderFactories(),
|
signoz.NewSQLStoreProviderFactories(),
|
||||||
signoz.NewTelemetryStoreProviderFactories(),
|
signoz.NewTelemetryStoreProviderFactories(),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/sharder"
|
"github.com/SigNoz/signoz/pkg/sharder"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlmigration"
|
"github.com/SigNoz/signoz/pkg/sqlmigration"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlmigrator"
|
"github.com/SigNoz/signoz/pkg/sqlmigrator"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||||
@ -57,6 +58,9 @@ type Config struct {
|
|||||||
// SQLMigrator config
|
// SQLMigrator config
|
||||||
SQLMigrator sqlmigrator.Config `mapstructure:"sqlmigrator"`
|
SQLMigrator sqlmigrator.Config `mapstructure:"sqlmigrator"`
|
||||||
|
|
||||||
|
// SQLSchema config
|
||||||
|
SQLSchema sqlschema.Config `mapstructure:"sqlschema"`
|
||||||
|
|
||||||
// API Server config
|
// API Server config
|
||||||
APIServer apiserver.Config `mapstructure:"apiserver"`
|
APIServer apiserver.Config `mapstructure:"apiserver"`
|
||||||
|
|
||||||
@ -111,6 +115,7 @@ func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, deprec
|
|||||||
cache.NewConfigFactory(),
|
cache.NewConfigFactory(),
|
||||||
sqlstore.NewConfigFactory(),
|
sqlstore.NewConfigFactory(),
|
||||||
sqlmigrator.NewConfigFactory(),
|
sqlmigrator.NewConfigFactory(),
|
||||||
|
sqlschema.NewConfigFactory(),
|
||||||
apiserver.NewConfigFactory(),
|
apiserver.NewConfigFactory(),
|
||||||
telemetrystore.NewConfigFactory(),
|
telemetrystore.NewConfigFactory(),
|
||||||
prometheus.NewConfigFactory(),
|
prometheus.NewConfigFactory(),
|
||||||
|
|||||||
@ -26,6 +26,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
"github.com/SigNoz/signoz/pkg/sharder/noopsharder"
|
||||||
"github.com/SigNoz/signoz/pkg/sharder/singlesharder"
|
"github.com/SigNoz/signoz/pkg/sharder/singlesharder"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlmigration"
|
"github.com/SigNoz/signoz/pkg/sqlmigration"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema/sqlitesqlschema"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlitesqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore/sqlitesqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||||
@ -69,7 +71,13 @@ func NewSQLStoreProviderFactories() factory.NamedMap[factory.ProviderFactory[sql
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlmigration.SQLMigration, sqlmigration.Config]] {
|
func NewSQLSchemaProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
|
||||||
|
return factory.MustNewNamedMap(
|
||||||
|
sqlitesqlschema.NewFactory(sqlstore),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.NamedMap[factory.ProviderFactory[sqlmigration.SQLMigration, sqlmigration.Config]] {
|
||||||
return factory.MustNewNamedMap(
|
return factory.MustNewNamedMap(
|
||||||
sqlmigration.NewAddDataMigrationsFactory(),
|
sqlmigration.NewAddDataMigrationsFactory(),
|
||||||
sqlmigration.NewAddOrganizationFactory(),
|
sqlmigration.NewAddOrganizationFactory(),
|
||||||
@ -112,6 +120,10 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM
|
|||||||
sqlmigration.NewDropFeatureSetFactory(),
|
sqlmigration.NewDropFeatureSetFactory(),
|
||||||
sqlmigration.NewDropDeprecatedTablesFactory(),
|
sqlmigration.NewDropDeprecatedTablesFactory(),
|
||||||
sqlmigration.NewUpdateAgentsFactory(sqlstore),
|
sqlmigration.NewUpdateAgentsFactory(sqlstore),
|
||||||
|
sqlmigration.NewUpdateUsersFactory(sqlstore, sqlschema),
|
||||||
|
sqlmigration.NewUpdateUserInviteFactory(sqlstore, sqlschema),
|
||||||
|
sqlmigration.NewUpdateOrgDomainFactory(sqlstore, sqlschema),
|
||||||
|
sqlmigration.NewAddFactorIndexesFactory(sqlstore, sqlschema),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema/sqlschematest"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
|
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstoretest"
|
||||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||||
@ -38,7 +40,7 @@ func TestNewProviderFactories(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
assert.NotPanics(t, func() {
|
assert.NotPanics(t, func() {
|
||||||
NewSQLMigrationProviderFactories(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual))
|
NewSQLMigrationProviderFactories(sqlstoretest.New(sqlstore.Config{Provider: "sqlite"}, sqlmock.QueryMatcherEqual), sqlschematest.New(map[string]*sqlschema.Table{}, map[string][]*sqlschema.UniqueConstraint{}, map[string]sqlschema.Index{}))
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.NotPanics(t, func() {
|
assert.NotPanics(t, func() {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/sharder"
|
"github.com/SigNoz/signoz/pkg/sharder"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlmigration"
|
"github.com/SigNoz/signoz/pkg/sqlmigration"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlmigrator"
|
"github.com/SigNoz/signoz/pkg/sqlmigrator"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/statsreporter"
|
"github.com/SigNoz/signoz/pkg/statsreporter"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||||
@ -61,6 +62,7 @@ func New(
|
|||||||
emailingProviderFactories factory.NamedMap[factory.ProviderFactory[emailing.Emailing, emailing.Config]],
|
emailingProviderFactories factory.NamedMap[factory.ProviderFactory[emailing.Emailing, emailing.Config]],
|
||||||
cacheProviderFactories factory.NamedMap[factory.ProviderFactory[cache.Cache, cache.Config]],
|
cacheProviderFactories factory.NamedMap[factory.ProviderFactory[cache.Cache, cache.Config]],
|
||||||
webProviderFactories factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]],
|
webProviderFactories factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]],
|
||||||
|
sqlSchemaProviderFactories func(sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]],
|
||||||
sqlstoreProviderFactories factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]],
|
sqlstoreProviderFactories factory.NamedMap[factory.ProviderFactory[sqlstore.SQLStore, sqlstore.Config]],
|
||||||
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
|
telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]],
|
||||||
) (*SigNoz, error) {
|
) (*SigNoz, error) {
|
||||||
@ -183,12 +185,23 @@ func New(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sqlschema, err := factory.NewProviderFromNamedMap(
|
||||||
|
ctx,
|
||||||
|
providerSettings,
|
||||||
|
config.SQLSchema,
|
||||||
|
sqlSchemaProviderFactories(sqlstore),
|
||||||
|
config.SQLStore.Provider,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Run migrations on the sqlstore
|
// Run migrations on the sqlstore
|
||||||
sqlmigrations, err := sqlmigration.New(
|
sqlmigrations, err := sqlmigration.New(
|
||||||
ctx,
|
ctx,
|
||||||
providerSettings,
|
providerSettings,
|
||||||
config.SQLMigration,
|
config.SQLMigration,
|
||||||
NewSQLMigrationProviderFactories(sqlstore),
|
NewSQLMigrationProviderFactories(sqlstore, sqlschema),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
85
pkg/sqlmigration/042_update_users.go
Normal file
85
pkg/sqlmigration/042_update_users.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package sqlmigration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateUsers struct {
|
||||||
|
sqlstore sqlstore.SQLStore
|
||||||
|
sqlschema sqlschema.SQLSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateUsersFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("update_users"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
|
||||||
|
return newUpdateUsers(ctx, providerSettings, config, sqlstore, sqlschema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpdateUsers(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
|
||||||
|
return &updateUsers{
|
||||||
|
sqlstore: sqlstore,
|
||||||
|
sqlschema: sqlschema,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *updateUsers) Register(migrations *migrate.Migrations) error {
|
||||||
|
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *updateUsers) Up(ctx context.Context, db *bun.DB) error {
|
||||||
|
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("users"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sqls := [][]byte{}
|
||||||
|
|
||||||
|
dropSQLs := migration.sqlschema.Operator().DropConstraint(table, uniqueConstraints, &sqlschema.UniqueConstraint{ColumnNames: []sqlschema.ColumnName{"email"}})
|
||||||
|
sqls = append(sqls, dropSQLs...)
|
||||||
|
|
||||||
|
indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "users", ColumnNames: []sqlschema.ColumnName{"email", "org_id"}})
|
||||||
|
sqls = append(sqls, indexSQLs...)
|
||||||
|
|
||||||
|
for _, sql := range sqls {
|
||||||
|
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *updateUsers) Down(ctx context.Context, db *bun.DB) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
88
pkg/sqlmigration/043_update_user_invite.go
Normal file
88
pkg/sqlmigration/043_update_user_invite.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package sqlmigration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateUserInvite struct {
|
||||||
|
sqlstore sqlstore.SQLStore
|
||||||
|
sqlschema sqlschema.SQLSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateUserInviteFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("update_user_invite"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
|
||||||
|
return newUpdateUserInvite(ctx, providerSettings, config, sqlstore, sqlschema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpdateUserInvite(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
|
||||||
|
return &updateUserInvite{
|
||||||
|
sqlstore: sqlstore,
|
||||||
|
sqlschema: sqlschema,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *updateUserInvite) Register(migrations *migrate.Migrations) error {
|
||||||
|
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *updateUserInvite) Up(ctx context.Context, db *bun.DB) error {
|
||||||
|
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("user_invite"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sqls := [][]byte{}
|
||||||
|
|
||||||
|
dropSQLs := migration.sqlschema.Operator().DropConstraint(table, uniqueConstraints, &sqlschema.UniqueConstraint{ColumnNames: []sqlschema.ColumnName{"email"}})
|
||||||
|
sqls = append(sqls, dropSQLs...)
|
||||||
|
|
||||||
|
indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "user_invite", ColumnNames: []sqlschema.ColumnName{"email", "org_id"}})
|
||||||
|
sqls = append(sqls, indexSQLs...)
|
||||||
|
|
||||||
|
indexSQLs = migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "user_invite", ColumnNames: []sqlschema.ColumnName{"token"}})
|
||||||
|
sqls = append(sqls, indexSQLs...)
|
||||||
|
|
||||||
|
for _, sql := range sqls {
|
||||||
|
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *updateUserInvite) Down(ctx context.Context, db *bun.DB) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
85
pkg/sqlmigration/044_update_org_domain.go
Normal file
85
pkg/sqlmigration/044_update_org_domain.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package sqlmigration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type updateOrgDomain struct {
|
||||||
|
sqlstore sqlstore.SQLStore
|
||||||
|
sqlschema sqlschema.SQLSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpdateOrgDomainFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("update_org_domain"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
|
||||||
|
return newUpdateOrgDomain(ctx, providerSettings, config, sqlstore, sqlschema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUpdateOrgDomain(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
|
||||||
|
return &updateOrgDomain{
|
||||||
|
sqlstore: sqlstore,
|
||||||
|
sqlschema: sqlschema,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *updateOrgDomain) Register(migrations *migrate.Migrations) error {
|
||||||
|
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *updateOrgDomain) Up(ctx context.Context, db *bun.DB) error {
|
||||||
|
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
table, uniqueConstraints, err := migration.sqlschema.GetTable(ctx, sqlschema.TableName("org_domains"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sqls := [][]byte{}
|
||||||
|
|
||||||
|
dropSQLs := migration.sqlschema.Operator().DropConstraint(table, uniqueConstraints, &sqlschema.UniqueConstraint{ColumnNames: []sqlschema.ColumnName{"name"}})
|
||||||
|
sqls = append(sqls, dropSQLs...)
|
||||||
|
|
||||||
|
indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "org_domains", ColumnNames: []sqlschema.ColumnName{"name", "org_id"}})
|
||||||
|
sqls = append(sqls, indexSQLs...)
|
||||||
|
|
||||||
|
for _, sql := range sqls {
|
||||||
|
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := migration.sqlschema.ToggleFKEnforcement(ctx, db, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *updateOrgDomain) Down(ctx context.Context, db *bun.DB) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
75
pkg/sqlmigration/045_add_factor_indexes.go
Normal file
75
pkg/sqlmigration/045_add_factor_indexes.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package sqlmigration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/migrate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type addFactorIndexes struct {
|
||||||
|
sqlstore sqlstore.SQLStore
|
||||||
|
sqlschema sqlschema.SQLSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAddFactorIndexesFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("add_factor_indexes"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
|
||||||
|
return newAddFactorIndexes(ctx, providerSettings, config, sqlstore, sqlschema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAddFactorIndexes(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
|
||||||
|
return &addFactorIndexes{
|
||||||
|
sqlstore: sqlstore,
|
||||||
|
sqlschema: sqlschema,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *addFactorIndexes) Register(migrations *migrate.Migrations) error {
|
||||||
|
if err := migrations.Register(migration.Up, migration.Down); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *addFactorIndexes) Up(ctx context.Context, db *bun.DB) error {
|
||||||
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
sqls := [][]byte{}
|
||||||
|
|
||||||
|
indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "factor_password", ColumnNames: []sqlschema.ColumnName{"user_id"}})
|
||||||
|
sqls = append(sqls, indexSQLs...)
|
||||||
|
|
||||||
|
indexSQLs = migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "reset_password_token", ColumnNames: []sqlschema.ColumnName{"password_id"}})
|
||||||
|
sqls = append(sqls, indexSQLs...)
|
||||||
|
|
||||||
|
indexSQLs = migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "reset_password_token", ColumnNames: []sqlschema.ColumnName{"token"}})
|
||||||
|
sqls = append(sqls, indexSQLs...)
|
||||||
|
|
||||||
|
for _, sql := range sqls {
|
||||||
|
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migration *addFactorIndexes) Down(ctx context.Context, db *bun.DB) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -14,8 +14,10 @@ type SQLMigration interface {
|
|||||||
// Register registers the migration with the given migrations. Each migration needs to be registered
|
// Register registers the migration with the given migrations. Each migration needs to be registered
|
||||||
//in a dedicated `*.go` file so that the correct migration semantics can be detected.
|
//in a dedicated `*.go` file so that the correct migration semantics can be detected.
|
||||||
Register(*migrate.Migrations) error
|
Register(*migrate.Migrations) error
|
||||||
|
|
||||||
// Up runs the migration.
|
// Up runs the migration.
|
||||||
Up(context.Context, *bun.DB) error
|
Up(context.Context, *bun.DB) error
|
||||||
|
|
||||||
// Down rolls back the migration.
|
// Down rolls back the migration.
|
||||||
Down(context.Context, *bun.DB) error
|
Down(context.Context, *bun.DB) error
|
||||||
}
|
}
|
||||||
@ -66,5 +68,6 @@ func MustNew(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return migrations
|
return migrations
|
||||||
}
|
}
|
||||||
|
|||||||
130
pkg/sqlschema/column.go
Normal file
130
pkg/sqlschema/column.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DataTypeText = DataType{s: valuer.NewString("TEXT"), z: ""}
|
||||||
|
DataTypeBigInt = DataType{s: valuer.NewString("BIGINT"), z: int64(0)}
|
||||||
|
DataTypeInteger = DataType{s: valuer.NewString("INTEGER"), z: int64(0)}
|
||||||
|
DataTypeNumeric = DataType{s: valuer.NewString("NUMERIC"), z: float64(0)}
|
||||||
|
DataTypeBoolean = DataType{s: valuer.NewString("BOOLEAN"), z: false}
|
||||||
|
DataTypeTimestamp = DataType{s: valuer.NewString("TIMESTAMP"), z: time.Time{}}
|
||||||
|
)
|
||||||
|
|
||||||
|
type DataType struct {
|
||||||
|
s valuer.String
|
||||||
|
z any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DataType) String() string {
|
||||||
|
return d.s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ColumnName string
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
// The name of the column in the table.
|
||||||
|
Name ColumnName
|
||||||
|
|
||||||
|
// The data type of the column. This will be translated to the the appropriate data type as per the dialect.
|
||||||
|
DataType DataType
|
||||||
|
|
||||||
|
// Whether the column is nullable.
|
||||||
|
Nullable bool
|
||||||
|
|
||||||
|
// The default value of the column.
|
||||||
|
Default string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (column *Column) ToDefinitionSQL(fmter SQLFormatter) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = fmter.AppendIdent(sql, string(column.Name))
|
||||||
|
sql = append(sql, " "...)
|
||||||
|
sql = append(sql, fmter.SQLDataTypeOf(column.DataType)...)
|
||||||
|
|
||||||
|
if !column.Nullable {
|
||||||
|
sql = append(sql, " NOT NULL"...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if column.Default != "" {
|
||||||
|
sql = append(sql, " DEFAULT "...)
|
||||||
|
sql = append(sql, column.Default...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (column *Column) ToAddSQL(fmter SQLFormatter, tableName TableName, ifNotExists bool) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "ALTER TABLE "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(tableName))
|
||||||
|
sql = append(sql, " ADD COLUMN "...)
|
||||||
|
if ifNotExists {
|
||||||
|
sql = append(sql, "IF NOT EXISTS "...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if column.Default == "" && !column.Nullable {
|
||||||
|
adjustedColumn := &Column{
|
||||||
|
Name: column.Name,
|
||||||
|
DataType: column.DataType,
|
||||||
|
Nullable: true,
|
||||||
|
Default: column.Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = append(sql, adjustedColumn.ToDefinitionSQL(fmter)...)
|
||||||
|
} else {
|
||||||
|
sql = append(sql, column.ToDefinitionSQL(fmter)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (column *Column) ToDropSQL(fmter SQLFormatter, tableName TableName, ifExists bool) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "ALTER TABLE "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(tableName))
|
||||||
|
sql = append(sql, " DROP COLUMN "...)
|
||||||
|
if ifExists {
|
||||||
|
sql = append(sql, "IF EXISTS "...)
|
||||||
|
}
|
||||||
|
sql = fmter.AppendIdent(sql, string(column.Name))
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (column *Column) ToUpdateSQL(fmter SQLFormatter, tableName TableName, value any) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "UPDATE "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(tableName))
|
||||||
|
sql = append(sql, " SET "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(column.Name))
|
||||||
|
sql = append(sql, " = "...)
|
||||||
|
|
||||||
|
if v, ok := value.(ColumnName); ok {
|
||||||
|
sql = fmter.AppendIdent(sql, string(v))
|
||||||
|
} else {
|
||||||
|
sql = fmter.AppendValue(sql, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (column *Column) ToSetNotNullSQL(fmter SQLFormatter, tableName TableName) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "ALTER TABLE "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(tableName))
|
||||||
|
sql = append(sql, " ALTER COLUMN "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(column.Name))
|
||||||
|
sql = append(sql, " SET NOT NULL"...)
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
118
pkg/sqlschema/column_test.go
Normal file
118
pkg/sqlschema/column_test.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/uptrace/bun/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestColumnToDefinitionSQL(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
column Column
|
||||||
|
sql string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "TimestampNotNullNoDefault",
|
||||||
|
column: Column{Name: "created_at", DataType: DataTypeTimestamp, Nullable: false},
|
||||||
|
sql: `"created_at" TIMESTAMP NOT NULL`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TimestampNotNullWithDefault",
|
||||||
|
column: Column{Name: "created_at", DataType: DataTypeTimestamp, Nullable: false, Default: "CURRENT_TIMESTAMP"},
|
||||||
|
sql: `"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TimestampNullableNoDefault",
|
||||||
|
column: Column{Name: "created_at", DataType: DataTypeTimestamp, Nullable: true},
|
||||||
|
sql: `"created_at" TIMESTAMP`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fmter := Formatter{schema.NewNopFormatter()}
|
||||||
|
sql := testCase.column.ToDefinitionSQL(fmter)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.sql, string(sql))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColumnToUpdateSQL(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
column Column
|
||||||
|
tableName TableName
|
||||||
|
value any
|
||||||
|
sql string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Timestamp",
|
||||||
|
column: Column{Name: "ts", DataType: DataTypeTimestamp},
|
||||||
|
tableName: "test",
|
||||||
|
value: time.Time{},
|
||||||
|
sql: `UPDATE "test" SET "ts" = '0001-01-01 00:00:00+00:00'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Integer",
|
||||||
|
column: Column{Name: "i", DataType: DataTypeInteger},
|
||||||
|
tableName: "test",
|
||||||
|
value: 1,
|
||||||
|
sql: `UPDATE "test" SET "i" = 1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Text",
|
||||||
|
column: Column{Name: "t", DataType: DataTypeText},
|
||||||
|
tableName: "test",
|
||||||
|
value: "test",
|
||||||
|
sql: `UPDATE "test" SET "t" = 'test'`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "BigInt",
|
||||||
|
column: Column{Name: "bi", DataType: DataTypeBigInt},
|
||||||
|
tableName: "test",
|
||||||
|
value: 1,
|
||||||
|
sql: `UPDATE "test" SET "bi" = 1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Numeric",
|
||||||
|
column: Column{Name: "n", DataType: DataTypeNumeric},
|
||||||
|
tableName: "test",
|
||||||
|
value: 1.1,
|
||||||
|
sql: `UPDATE "test" SET "n" = 1.1`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Boolean",
|
||||||
|
column: Column{Name: "b", DataType: DataTypeBoolean},
|
||||||
|
tableName: "test",
|
||||||
|
value: true,
|
||||||
|
sql: `UPDATE "test" SET "b" = TRUE`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Null",
|
||||||
|
column: Column{Name: "n", DataType: DataTypeNumeric},
|
||||||
|
tableName: "test",
|
||||||
|
value: nil,
|
||||||
|
sql: `UPDATE "test" SET "n" = NULL`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ColumnName",
|
||||||
|
column: Column{Name: "n", DataType: DataTypeNumeric},
|
||||||
|
tableName: "test",
|
||||||
|
value: ColumnName("n"),
|
||||||
|
sql: `UPDATE "test" SET "n" = "n"`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fmter := Formatter{schema.NewNopFormatter()}
|
||||||
|
sql := testCase.column.ToUpdateSQL(fmter, testCase.tableName, testCase.value)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.sql, string(sql))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
17
pkg/sqlschema/config.go
Normal file
17
pkg/sqlschema/config.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import "github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
|
||||||
|
type Config struct{}
|
||||||
|
|
||||||
|
func NewConfigFactory() factory.ConfigFactory {
|
||||||
|
return factory.NewConfigFactory(factory.MustNewName("sqlschema"), newConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfig() factory.Config {
|
||||||
|
return Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
325
pkg/sqlschema/constraint.go
Normal file
325
pkg/sqlschema/constraint.go
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ConstraintTypePrimaryKey = ConstraintType{s: valuer.NewString("pk")}
|
||||||
|
ConstraintTypeForeignKey = ConstraintType{s: valuer.NewString("fk")}
|
||||||
|
ConstraintTypeCheck = ConstraintType{s: valuer.NewString("ck")}
|
||||||
|
ConstraintTypeUnique = ConstraintType{s: valuer.NewString("uq")}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConstraintType struct{ s valuer.String }
|
||||||
|
|
||||||
|
func (c ConstraintType) String() string {
|
||||||
|
return c.s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Constraint = (*PrimaryKeyConstraint)(nil)
|
||||||
|
_ Constraint = (*ForeignKeyConstraint)(nil)
|
||||||
|
_ Constraint = (*UniqueConstraint)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Constraint interface {
|
||||||
|
// The name of the constraint. This will be autogenerated and should not be set by the user.
|
||||||
|
// - Primary keys are named as `pk_<table_name>`.
|
||||||
|
// - Foreign key constraints are named as `fk_<table_name>_<column_name>`.
|
||||||
|
// - Check constraints are named as `ck_<table_name>_<name>`. The name is the name of the check constraint.
|
||||||
|
Name(tableName TableName) string
|
||||||
|
|
||||||
|
// Add name to the constraint. This is typically used to override the autogenerated name because the database might have a different name.
|
||||||
|
Named(name string) Constraint
|
||||||
|
|
||||||
|
// The type of the constraint.
|
||||||
|
Type() ConstraintType
|
||||||
|
|
||||||
|
// The columns that the constraint is applied to.
|
||||||
|
Columns() []ColumnName
|
||||||
|
|
||||||
|
// Equals returns true if the constraint is equal to the other constraint.
|
||||||
|
Equals(other Constraint) bool
|
||||||
|
|
||||||
|
// The SQL representation of the constraint.
|
||||||
|
ToDefinitionSQL(fmter SQLFormatter, tableName TableName) []byte
|
||||||
|
|
||||||
|
// The SQL representation of the constraint.
|
||||||
|
ToDropSQL(fmter SQLFormatter, tableName TableName) []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrimaryKeyConstraint struct {
|
||||||
|
ColumnNames []ColumnName
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *PrimaryKeyConstraint) Name(tableName TableName) string {
|
||||||
|
if constraint.name != "" {
|
||||||
|
return constraint.name
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(ConstraintTypePrimaryKey.String())
|
||||||
|
b.WriteString("_")
|
||||||
|
b.WriteString(string(tableName))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *PrimaryKeyConstraint) Named(name string) Constraint {
|
||||||
|
copyOfColumnNames := make([]ColumnName, len(constraint.ColumnNames))
|
||||||
|
copy(copyOfColumnNames, constraint.ColumnNames)
|
||||||
|
|
||||||
|
return &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: copyOfColumnNames,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *PrimaryKeyConstraint) Type() ConstraintType {
|
||||||
|
return ConstraintTypePrimaryKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *PrimaryKeyConstraint) Columns() []ColumnName {
|
||||||
|
return constraint.ColumnNames
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *PrimaryKeyConstraint) Equals(other Constraint) bool {
|
||||||
|
if other.Type() != ConstraintTypePrimaryKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(constraint.ColumnNames) != len(other.Columns()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
foundColumns := make(map[ColumnName]bool)
|
||||||
|
for _, column := range constraint.ColumnNames {
|
||||||
|
foundColumns[column] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, column := range other.Columns() {
|
||||||
|
if !foundColumns[column] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *PrimaryKeyConstraint) ToDefinitionSQL(fmter SQLFormatter, tableName TableName) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "CONSTRAINT "...)
|
||||||
|
sql = fmter.AppendIdent(sql, constraint.Name(tableName))
|
||||||
|
sql = append(sql, " PRIMARY KEY ("...)
|
||||||
|
|
||||||
|
for i, column := range constraint.ColumnNames {
|
||||||
|
if i > 0 {
|
||||||
|
sql = append(sql, ", "...)
|
||||||
|
}
|
||||||
|
sql = fmter.AppendIdent(sql, string(column))
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = append(sql, ")"...)
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *PrimaryKeyConstraint) ToDropSQL(fmter SQLFormatter, tableName TableName) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "ALTER TABLE "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(tableName))
|
||||||
|
sql = append(sql, " DROP CONSTRAINT IF EXISTS "...)
|
||||||
|
sql = fmter.AppendIdent(sql, constraint.Name(tableName))
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForeignKeyConstraint struct {
|
||||||
|
ReferencingColumnName ColumnName
|
||||||
|
ReferencedTableName TableName
|
||||||
|
ReferencedColumnName ColumnName
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *ForeignKeyConstraint) Name(tableName TableName) string {
|
||||||
|
if constraint.name != "" {
|
||||||
|
return constraint.name
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(ConstraintTypeForeignKey.String())
|
||||||
|
b.WriteString("_")
|
||||||
|
b.WriteString(string(tableName))
|
||||||
|
b.WriteString("_")
|
||||||
|
b.WriteString(string(constraint.ReferencingColumnName))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *ForeignKeyConstraint) Named(name string) Constraint {
|
||||||
|
return &ForeignKeyConstraint{
|
||||||
|
ReferencingColumnName: constraint.ReferencingColumnName,
|
||||||
|
ReferencedTableName: constraint.ReferencedTableName,
|
||||||
|
ReferencedColumnName: constraint.ReferencedColumnName,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *ForeignKeyConstraint) Type() ConstraintType {
|
||||||
|
return ConstraintTypeForeignKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *ForeignKeyConstraint) Columns() []ColumnName {
|
||||||
|
return []ColumnName{constraint.ReferencingColumnName}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *ForeignKeyConstraint) Equals(other Constraint) bool {
|
||||||
|
if other.Type() != ConstraintTypeForeignKey {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
otherForeignKeyConstraint, ok := other.(*ForeignKeyConstraint)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return constraint.ReferencingColumnName == otherForeignKeyConstraint.ReferencingColumnName &&
|
||||||
|
constraint.ReferencedTableName == otherForeignKeyConstraint.ReferencedTableName &&
|
||||||
|
constraint.ReferencedColumnName == otherForeignKeyConstraint.ReferencedColumnName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *ForeignKeyConstraint) ToDefinitionSQL(fmter SQLFormatter, tableName TableName) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "CONSTRAINT "...)
|
||||||
|
sql = fmter.AppendIdent(sql, constraint.Name(tableName))
|
||||||
|
sql = append(sql, " FOREIGN KEY ("...)
|
||||||
|
|
||||||
|
sql = fmter.AppendIdent(sql, string(constraint.ReferencingColumnName))
|
||||||
|
sql = append(sql, ") REFERENCES "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(constraint.ReferencedTableName))
|
||||||
|
sql = append(sql, " ("...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(constraint.ReferencedColumnName))
|
||||||
|
sql = append(sql, ")"...)
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *ForeignKeyConstraint) ToDropSQL(fmter SQLFormatter, tableName TableName) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "ALTER TABLE "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(tableName))
|
||||||
|
sql = append(sql, " DROP CONSTRAINT IF EXISTS "...)
|
||||||
|
sql = fmter.AppendIdent(sql, constraint.Name(tableName))
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not use this constraint type. Instead create an index with the `UniqueIndex` type.
|
||||||
|
// The main difference between a Unique Index and a Unique Constraint is mostly semantic, with a constraint focusing more on data integrity, while an index focuses on performance.
|
||||||
|
// We choose to create unique indices because of sqlite. Dropping a unique index is directly supported whilst dropping a unique constraint requires a recreation of the table with the constraint removed.
|
||||||
|
type UniqueConstraint struct {
|
||||||
|
ColumnNames []ColumnName
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *UniqueConstraint) Name(tableName TableName) string {
|
||||||
|
if constraint.name != "" {
|
||||||
|
return constraint.name
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(ConstraintTypeUnique.String())
|
||||||
|
b.WriteString("_")
|
||||||
|
b.WriteString(string(tableName))
|
||||||
|
b.WriteString("_")
|
||||||
|
for i, column := range constraint.ColumnNames {
|
||||||
|
if i > 0 {
|
||||||
|
b.WriteString("_")
|
||||||
|
}
|
||||||
|
b.WriteString(string(column))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *UniqueConstraint) Named(name string) Constraint {
|
||||||
|
copyOfColumnNames := make([]ColumnName, len(constraint.ColumnNames))
|
||||||
|
copy(copyOfColumnNames, constraint.ColumnNames)
|
||||||
|
|
||||||
|
return &UniqueConstraint{
|
||||||
|
ColumnNames: copyOfColumnNames,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *UniqueConstraint) Type() ConstraintType {
|
||||||
|
return ConstraintTypeUnique
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *UniqueConstraint) Columns() []ColumnName {
|
||||||
|
return constraint.ColumnNames
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *UniqueConstraint) Equals(other Constraint) bool {
|
||||||
|
if other.Type() != ConstraintTypeUnique {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
foundColumns := make(map[ColumnName]bool)
|
||||||
|
for _, column := range constraint.ColumnNames {
|
||||||
|
foundColumns[column] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, column := range other.Columns() {
|
||||||
|
if !foundColumns[column] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *UniqueConstraint) ToIndex(tableName TableName) *UniqueIndex {
|
||||||
|
copyOfColumnNames := make([]ColumnName, len(constraint.ColumnNames))
|
||||||
|
copy(copyOfColumnNames, constraint.ColumnNames)
|
||||||
|
|
||||||
|
return &UniqueIndex{
|
||||||
|
TableName: tableName,
|
||||||
|
ColumnNames: copyOfColumnNames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *UniqueConstraint) ToDefinitionSQL(fmter SQLFormatter, tableName TableName) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "CONSTRAINT "...)
|
||||||
|
sql = fmter.AppendIdent(sql, constraint.Name(tableName))
|
||||||
|
sql = append(sql, " UNIQUE ("...)
|
||||||
|
|
||||||
|
for i, column := range constraint.ColumnNames {
|
||||||
|
if i > 0 {
|
||||||
|
sql = append(sql, ", "...)
|
||||||
|
}
|
||||||
|
sql = fmter.AppendIdent(sql, string(column))
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = append(sql, ")"...)
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (constraint *UniqueConstraint) ToDropSQL(fmter SQLFormatter, tableName TableName) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "ALTER TABLE "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(tableName))
|
||||||
|
sql = append(sql, " DROP CONSTRAINT IF EXISTS "...)
|
||||||
|
sql = fmter.AppendIdent(sql, constraint.Name(tableName))
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
72
pkg/sqlschema/constraint_test.go
Normal file
72
pkg/sqlschema/constraint_test.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/uptrace/bun/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrimaryKeyConstraintToDefinitionSQL(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
tableName TableName
|
||||||
|
constraint *PrimaryKeyConstraint
|
||||||
|
sql string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "SingleColumn",
|
||||||
|
tableName: "test",
|
||||||
|
constraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
sql: `CONSTRAINT "pk_test" PRIMARY KEY ("id")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MultipleColumns",
|
||||||
|
tableName: "test",
|
||||||
|
constraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id", "name"},
|
||||||
|
},
|
||||||
|
sql: `CONSTRAINT "pk_test" PRIMARY KEY ("id", "name")`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fmter := Formatter{schema.NewNopFormatter()}
|
||||||
|
sql := testCase.constraint.ToDefinitionSQL(fmter, testCase.tableName)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.sql, string(sql))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForeignKeyConstraintToDefinitionSQL(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
tableName TableName
|
||||||
|
constraint *ForeignKeyConstraint
|
||||||
|
sql string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "NoCascade",
|
||||||
|
tableName: "test",
|
||||||
|
constraint: &ForeignKeyConstraint{
|
||||||
|
ReferencingColumnName: "id",
|
||||||
|
ReferencedTableName: "test_referenced",
|
||||||
|
ReferencedColumnName: "id",
|
||||||
|
},
|
||||||
|
sql: `CONSTRAINT "fk_test_id" FOREIGN KEY ("id") REFERENCES "test_referenced" ("id")`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fmter := Formatter{schema.NewNopFormatter()}
|
||||||
|
sql := testCase.constraint.ToDefinitionSQL(fmter, testCase.tableName)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.sql, string(sql))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
49
pkg/sqlschema/formatter.go
Normal file
49
pkg/sqlschema/formatter.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
"github.com/uptrace/bun/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ SQLFormatter = (*Formatter)(nil)
|
||||||
|
|
||||||
|
type Formatter struct {
|
||||||
|
bunf schema.Formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFormatter(dialect schema.Dialect) Formatter {
|
||||||
|
return Formatter{bunf: schema.NewFormatter(dialect)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter Formatter) SQLDataTypeOf(dataType DataType) string {
|
||||||
|
return strings.ToUpper(dataType.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter Formatter) DataTypeOf(dataType string) DataType {
|
||||||
|
switch strings.ToUpper(dataType) {
|
||||||
|
case "TEXT":
|
||||||
|
return DataTypeText
|
||||||
|
case "BIGINT":
|
||||||
|
return DataTypeBigInt
|
||||||
|
case "INTEGER":
|
||||||
|
return DataTypeInteger
|
||||||
|
case "NUMERIC":
|
||||||
|
return DataTypeNumeric
|
||||||
|
case "BOOLEAN":
|
||||||
|
return DataTypeBoolean
|
||||||
|
case "TIMESTAMP":
|
||||||
|
return DataTypeTimestamp
|
||||||
|
default:
|
||||||
|
return DataType{s: valuer.NewString(dataType)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter Formatter) AppendIdent(b []byte, ident string) []byte {
|
||||||
|
return formatter.bunf.AppendIdent(b, ident)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter Formatter) AppendValue(b []byte, v any) []byte {
|
||||||
|
return schema.Append(formatter.bunf, b, v)
|
||||||
|
}
|
||||||
116
pkg/sqlschema/index.go
Normal file
116
pkg/sqlschema/index.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
IndexTypeUnique = IndexType{s: valuer.NewString("uq")}
|
||||||
|
IndexTypeIndex = IndexType{s: valuer.NewString("ix")}
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexType struct{ s valuer.String }
|
||||||
|
|
||||||
|
func (i IndexType) String() string {
|
||||||
|
return i.s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Index interface {
|
||||||
|
// The name of the index.
|
||||||
|
// - Indexes are named as `ix_<table_name>_<column_names>`. The column names are separated by underscores.
|
||||||
|
// - Unique constraints are named as `uq_<table_name>_<column_names>`. The column names are separated by underscores.
|
||||||
|
// The name is autogenerated and should not be set by the user.
|
||||||
|
Name() string
|
||||||
|
|
||||||
|
// Add name to the index. This is typically used to override the autogenerated name because the database might have a different name.
|
||||||
|
Named(name string) Index
|
||||||
|
|
||||||
|
// The type of the index.
|
||||||
|
Type() IndexType
|
||||||
|
|
||||||
|
// The columns that the index is applied to.
|
||||||
|
Columns() []ColumnName
|
||||||
|
|
||||||
|
// The SQL representation of the index.
|
||||||
|
ToCreateSQL(fmter SQLFormatter) []byte
|
||||||
|
|
||||||
|
// Drop the index.
|
||||||
|
ToDropSQL(fmter SQLFormatter) []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniqueIndex struct {
|
||||||
|
TableName TableName
|
||||||
|
ColumnNames []ColumnName
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *UniqueIndex) Name() string {
|
||||||
|
if index.name != "" {
|
||||||
|
return index.name
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(IndexTypeUnique.String())
|
||||||
|
b.WriteString("_")
|
||||||
|
b.WriteString(string(index.TableName))
|
||||||
|
b.WriteString("_")
|
||||||
|
for i, column := range index.ColumnNames {
|
||||||
|
if i > 0 {
|
||||||
|
b.WriteString("_")
|
||||||
|
}
|
||||||
|
b.WriteString(string(column))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *UniqueIndex) Named(name string) Index {
|
||||||
|
copyOfColumnNames := make([]ColumnName, len(index.ColumnNames))
|
||||||
|
copy(copyOfColumnNames, index.ColumnNames)
|
||||||
|
|
||||||
|
return &UniqueIndex{
|
||||||
|
TableName: index.TableName,
|
||||||
|
ColumnNames: copyOfColumnNames,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UniqueIndex) Type() IndexType {
|
||||||
|
return IndexTypeUnique
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *UniqueIndex) Columns() []ColumnName {
|
||||||
|
return index.ColumnNames
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *UniqueIndex) ToCreateSQL(fmter SQLFormatter) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "CREATE UNIQUE INDEX IF NOT EXISTS "...)
|
||||||
|
sql = fmter.AppendIdent(sql, index.Name())
|
||||||
|
sql = append(sql, " ON "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(index.TableName))
|
||||||
|
sql = append(sql, " ("...)
|
||||||
|
|
||||||
|
for i, column := range index.ColumnNames {
|
||||||
|
if i > 0 {
|
||||||
|
sql = append(sql, ", "...)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = fmter.AppendIdent(sql, string(column))
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = append(sql, ")"...)
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (index *UniqueIndex) ToDropSQL(fmter SQLFormatter) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "DROP INDEX IF EXISTS "...)
|
||||||
|
sql = fmter.AppendIdent(sql, index.Name())
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
51
pkg/sqlschema/index_test.go
Normal file
51
pkg/sqlschema/index_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/uptrace/bun/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIndexToCreateSQL(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
index Index
|
||||||
|
sql string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Unique_1Column",
|
||||||
|
index: &UniqueIndex{
|
||||||
|
TableName: "users",
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "uq_users_id" ON "users" ("id")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unique_2Columns",
|
||||||
|
index: &UniqueIndex{
|
||||||
|
TableName: "users",
|
||||||
|
ColumnNames: []ColumnName{"id", "name"},
|
||||||
|
},
|
||||||
|
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "uq_users_id_name" ON "users" ("id", "name")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Unique_3Columns_Named",
|
||||||
|
index: &UniqueIndex{
|
||||||
|
TableName: "users",
|
||||||
|
ColumnNames: []ColumnName{"id", "name", "email"},
|
||||||
|
name: "my_index",
|
||||||
|
},
|
||||||
|
sql: `CREATE UNIQUE INDEX IF NOT EXISTS "my_index" ON "users" ("id", "name", "email")`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fmter := Formatter{schema.NewNopFormatter()}
|
||||||
|
sql := testCase.index.ToCreateSQL(fmter)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.sql, string(sql))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
143
pkg/sqlschema/operator.go
Normal file
143
pkg/sqlschema/operator.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
var _ SQLOperator = (*Operator)(nil)
|
||||||
|
|
||||||
|
type OperatorSupport struct {
|
||||||
|
DropConstraint bool
|
||||||
|
ColumnIfNotExistsExists bool
|
||||||
|
AlterColumnSetNotNull bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Operator struct {
|
||||||
|
fmter SQLFormatter
|
||||||
|
support OperatorSupport
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOperator(fmter SQLFormatter, support OperatorSupport) *Operator {
|
||||||
|
return &Operator{
|
||||||
|
fmter: fmter,
|
||||||
|
support: support,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (operator *Operator) CreateTable(table *Table) [][]byte {
|
||||||
|
return [][]byte{table.ToCreateSQL(operator.fmter)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (operator *Operator) RenameTable(table *Table, newName TableName) [][]byte {
|
||||||
|
table.Name = newName
|
||||||
|
return [][]byte{table.ToRenameSQL(operator.fmter, newName)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (operator *Operator) RecreateTable(table *Table, uniqueConstraints []*UniqueConstraint) [][]byte {
|
||||||
|
sqls := [][]byte{}
|
||||||
|
|
||||||
|
sqls = append(sqls, table.ToCreateTempInsertDropAlterSQL(operator.fmter)...)
|
||||||
|
|
||||||
|
for _, uniqueConstraint := range uniqueConstraints {
|
||||||
|
sqls = append(sqls, uniqueConstraint.ToIndex(table.Name).ToCreateSQL(operator.fmter))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqls
|
||||||
|
}
|
||||||
|
|
||||||
|
func (operator *Operator) DropTable(table *Table) [][]byte {
|
||||||
|
return [][]byte{table.ToDropSQL(operator.fmter)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (operator *Operator) CreateIndex(index Index) [][]byte {
|
||||||
|
return [][]byte{index.ToCreateSQL(operator.fmter)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (operator *Operator) DropIndex(index Index) [][]byte {
|
||||||
|
return [][]byte{index.ToDropSQL(operator.fmter)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (operator *Operator) AddColumn(table *Table, uniqueConstraints []*UniqueConstraint, column *Column, val any) [][]byte {
|
||||||
|
// If the column already exists, we do not need to add it.
|
||||||
|
if index := operator.findColumnByName(table, column.Name); index != -1 {
|
||||||
|
return [][]byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the column to the table.
|
||||||
|
table.Columns = append(table.Columns, column)
|
||||||
|
|
||||||
|
sqls := [][]byte{
|
||||||
|
column.ToAddSQL(operator.fmter, table.Name, operator.support.ColumnIfNotExistsExists),
|
||||||
|
}
|
||||||
|
|
||||||
|
if !column.Nullable {
|
||||||
|
if val == nil {
|
||||||
|
val = column.DataType.z
|
||||||
|
}
|
||||||
|
sqls = append(sqls, column.ToUpdateSQL(operator.fmter, table.Name, val))
|
||||||
|
|
||||||
|
if operator.support.AlterColumnSetNotNull {
|
||||||
|
sqls = append(sqls, column.ToSetNotNullSQL(operator.fmter, table.Name))
|
||||||
|
} else {
|
||||||
|
sqls = append(sqls, operator.RecreateTable(table, uniqueConstraints)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqls
|
||||||
|
}
|
||||||
|
|
||||||
|
func (operator *Operator) DropColumn(table *Table, column *Column) [][]byte {
|
||||||
|
index := operator.findColumnByName(table, column.Name)
|
||||||
|
// If the column does not exist, we do not need to drop it.
|
||||||
|
if index == -1 {
|
||||||
|
return [][]byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Columns = append(table.Columns[:index], table.Columns[index+1:]...)
|
||||||
|
|
||||||
|
return [][]byte{column.ToDropSQL(operator.fmter, table.Name, operator.support.ColumnIfNotExistsExists)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (operator *Operator) DropConstraint(table *Table, uniqueConstraints []*UniqueConstraint, constraint Constraint) [][]byte {
|
||||||
|
// The name of the input constraint is not guaranteed to be the same as the name of the constraint in the database.
|
||||||
|
// So we need to find the constraint in the database and drop it.
|
||||||
|
toDropConstraint, found := table.DropConstraint(constraint)
|
||||||
|
if !found {
|
||||||
|
uniqueConstraintIndex := operator.findUniqueConstraint(uniqueConstraints, constraint)
|
||||||
|
if uniqueConstraintIndex == -1 {
|
||||||
|
return [][]byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if operator.support.DropConstraint {
|
||||||
|
return [][]byte{uniqueConstraints[uniqueConstraintIndex].ToDropSQL(operator.fmter, table.Name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return operator.RecreateTable(table, append(uniqueConstraints[:uniqueConstraintIndex], uniqueConstraints[uniqueConstraintIndex+1:]...))
|
||||||
|
}
|
||||||
|
|
||||||
|
if operator.support.DropConstraint {
|
||||||
|
return [][]byte{toDropConstraint.ToDropSQL(operator.fmter, table.Name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return operator.RecreateTable(table, uniqueConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Operator) findColumnByName(table *Table, columnName ColumnName) int {
|
||||||
|
for i, column := range table.Columns {
|
||||||
|
if column.Name == columnName {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Operator) findUniqueConstraint(uniqueConstraints []*UniqueConstraint, constraint Constraint) int {
|
||||||
|
if constraint.Type() != ConstraintTypeUnique {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, uniqueConstraint := range uniqueConstraints {
|
||||||
|
if uniqueConstraint.Equals(constraint) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
702
pkg/sqlschema/operator_test.go
Normal file
702
pkg/sqlschema/operator_test.go
Normal file
@ -0,0 +1,702 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/uptrace/bun/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// See table_test.go for more test cases on creating tables.
|
||||||
|
func TestOperatorCreateTable(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
table *Table
|
||||||
|
expectedSQLs [][]byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "PrimaryKey_ForeignKey_NotNullable",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`CREATE TABLE IF NOT EXISTS "users" ("id" INTEGER NOT NULL, "name" TEXT NOT NULL, "org_id" INTEGER NOT NULL, CONSTRAINT "pk_users" PRIMARY KEY ("id"), CONSTRAINT "fk_users_org_id" FOREIGN KEY ("org_id") REFERENCES "orgs" ("id"))`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fmter := NewFormatter(schema.NewNopFormatter().Dialect())
|
||||||
|
operator := NewOperator(fmter, OperatorSupport{})
|
||||||
|
|
||||||
|
actuals := operator.CreateTable(testCase.table)
|
||||||
|
assert.Equal(t, testCase.expectedSQLs, actuals)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOperatorAddColumn(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
table *Table
|
||||||
|
column *Column
|
||||||
|
val any
|
||||||
|
uniqueConstraints []*UniqueConstraint
|
||||||
|
support OperatorSupport
|
||||||
|
expectedSQLs [][]byte
|
||||||
|
expectedTable *Table
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "NullableNoDefault_DoesNotExist",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: &Column{Name: "name", DataType: DataTypeText, Nullable: true, Default: ""},
|
||||||
|
val: nil,
|
||||||
|
support: OperatorSupport{
|
||||||
|
ColumnIfNotExistsExists: true,
|
||||||
|
AlterColumnSetNotNull: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "name" TEXT`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: true, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MismatchingDataType_DoesExist",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: true, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: &Column{Name: "name", DataType: DataTypeBigInt, Nullable: true, Default: ""},
|
||||||
|
val: nil,
|
||||||
|
support: OperatorSupport{
|
||||||
|
ColumnIfNotExistsExists: true,
|
||||||
|
AlterColumnSetNotNull: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: true, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NotNullableNoDefaultNoVal_DoesNotExist",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: &Column{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
val: nil,
|
||||||
|
support: OperatorSupport{
|
||||||
|
ColumnIfNotExistsExists: true,
|
||||||
|
AlterColumnSetNotNull: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "name" TEXT`),
|
||||||
|
[]byte(`UPDATE "users" SET "name" = ''`),
|
||||||
|
[]byte(`ALTER TABLE "users" ALTER COLUMN "name" SET NOT NULL`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NotNullableNoDefault_DoesNotExist",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: &Column{Name: "num", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
val: int64(100),
|
||||||
|
support: OperatorSupport{
|
||||||
|
ColumnIfNotExistsExists: true,
|
||||||
|
AlterColumnSetNotNull: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "num" INTEGER`),
|
||||||
|
[]byte(`UPDATE "users" SET "num" = 100`),
|
||||||
|
[]byte(`ALTER TABLE "users" ALTER COLUMN "num" SET NOT NULL`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "num", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NotNullableNoDefault_DoesNotExist_AlterColumnSetNotNullFalse",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: &Column{Name: "num", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
val: int64(100),
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"name"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
ColumnIfNotExistsExists: true,
|
||||||
|
AlterColumnSetNotNull: false,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "num" INTEGER`),
|
||||||
|
[]byte(`UPDATE "users" SET "num" = 100`),
|
||||||
|
[]byte(`CREATE TABLE IF NOT EXISTS "users__temp" ("id" INTEGER NOT NULL, "name" TEXT NOT NULL, "num" INTEGER NOT NULL)`),
|
||||||
|
[]byte(`INSERT INTO "users__temp" ("id", "name", "num") SELECT "id", "name", "num" FROM "users"`),
|
||||||
|
[]byte(`DROP TABLE IF EXISTS "users"`),
|
||||||
|
[]byte(`ALTER TABLE "users__temp" RENAME TO "users"`),
|
||||||
|
[]byte(`CREATE UNIQUE INDEX IF NOT EXISTS "uq_users_name" ON "users" ("name")`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
{Name: "num", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MismatchingDataType_DoesExist_AlterColumnSetNotNullFalse",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: true, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
column: &Column{Name: "name", DataType: DataTypeBigInt, Nullable: false, Default: ""},
|
||||||
|
val: nil,
|
||||||
|
support: OperatorSupport{
|
||||||
|
ColumnIfNotExistsExists: true,
|
||||||
|
AlterColumnSetNotNull: false,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: true, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fmter := NewFormatter(schema.NewNopFormatter().Dialect())
|
||||||
|
operator := NewOperator(fmter, testCase.support)
|
||||||
|
|
||||||
|
actuals := operator.AddColumn(testCase.table, testCase.uniqueConstraints, testCase.column, testCase.val)
|
||||||
|
assert.Equal(t, testCase.expectedSQLs, actuals)
|
||||||
|
assert.Equal(t, testCase.expectedTable, testCase.table)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOperatorDropConstraint(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
table *Table
|
||||||
|
constraint Constraint
|
||||||
|
uniqueConstraints []*UniqueConstraint
|
||||||
|
support OperatorSupport
|
||||||
|
expectedSQLs [][]byte
|
||||||
|
expectedTable *Table
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "PrimaryKeyConstraint_DoesExist_DropConstraintTrue",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`ALTER TABLE "users" DROP CONSTRAINT IF EXISTS "pk_users"`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PrimaryKeyConstraint_DoesNotExist_DropConstraintTrue",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PrimaryKeyConstraintDifferentName_DoesExist_DropConstraintTrue",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
name: "pk_users_different_name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`ALTER TABLE "users" DROP CONSTRAINT IF EXISTS "pk_users_different_name"`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PrimaryKeyConstraint_DoesExist_DropConstraintFalse",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: false,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`CREATE TABLE IF NOT EXISTS "users__temp" ("id" INTEGER NOT NULL)`),
|
||||||
|
[]byte(`INSERT INTO "users__temp" ("id") SELECT "id" FROM "users"`),
|
||||||
|
[]byte(`DROP TABLE IF EXISTS "users"`),
|
||||||
|
[]byte(`ALTER TABLE "users__temp" RENAME TO "users"`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PrimaryKeyConstraint_DoesNotExist_DropConstraintFalse",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: false,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UniqueConstraint_DoesExist_DropConstraintTrue",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &UniqueConstraint{
|
||||||
|
ColumnNames: []ColumnName{"name"},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"name"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`ALTER TABLE "users" DROP CONSTRAINT IF EXISTS "uq_users_name"`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UniqueConstraint_DoesNotExist_DropConstraintTrue",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &UniqueConstraint{
|
||||||
|
ColumnNames: []ColumnName{"name"},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"id"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UniqueConstraint_DoesExist_DropConstraintFalse",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &UniqueConstraint{
|
||||||
|
ColumnNames: []ColumnName{"name"},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"name"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: false,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`CREATE TABLE IF NOT EXISTS "users__temp" ("id" INTEGER NOT NULL, "name" TEXT NOT NULL)`),
|
||||||
|
[]byte(`INSERT INTO "users__temp" ("id", "name") SELECT "id", "name" FROM "users"`),
|
||||||
|
[]byte(`DROP TABLE IF EXISTS "users"`),
|
||||||
|
[]byte(`ALTER TABLE "users__temp" RENAME TO "users"`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UniqueConstraint_DoesNotExist_DropConstraintFalse",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &UniqueConstraint{
|
||||||
|
ColumnNames: []ColumnName{"name"},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"id"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: false,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ForeignKeyConstraint_DoesExist_DropConstraintTrue",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &ForeignKeyConstraint{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"id"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`ALTER TABLE "users" DROP CONSTRAINT IF EXISTS "fk_users_org_id"`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ForeignKeyConstraintDifferentName_DoesExist_DropConstraintTrue",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id", name: "my_fk"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &ForeignKeyConstraint{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"id"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`ALTER TABLE "users" DROP CONSTRAINT IF EXISTS "my_fk"`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ForeignKeyConstraint_DoesNotExist_DropConstraintTrue",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Note that the name of the referencing column is different from the one in the table.
|
||||||
|
constraint: &ForeignKeyConstraint{ReferencingColumnName: "orgid", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"id"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: true,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ForeignKeyConstraint_DoesExist_DropConstraintFalse",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &ForeignKeyConstraint{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"id"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: false,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`CREATE TABLE IF NOT EXISTS "users__temp" ("id" INTEGER NOT NULL, "org_id" INTEGER NOT NULL)`),
|
||||||
|
[]byte(`INSERT INTO "users__temp" ("id", "org_id") SELECT "id", "org_id" FROM "users"`),
|
||||||
|
[]byte(`DROP TABLE IF EXISTS "users"`),
|
||||||
|
[]byte(`ALTER TABLE "users__temp" RENAME TO "users"`),
|
||||||
|
// Note that a unique index is created because a unique constraint already existed for the table.
|
||||||
|
[]byte(`CREATE UNIQUE INDEX IF NOT EXISTS "uq_users_id" ON "users" ("id")`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ForeignKeyConstraintDifferentName_DoesExist_DropConstraintFalse",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id", name: "my_fk"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
constraint: &ForeignKeyConstraint{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"id"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: false,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{
|
||||||
|
[]byte(`CREATE TABLE IF NOT EXISTS "users__temp" ("id" INTEGER NOT NULL, "org_id" INTEGER NOT NULL)`),
|
||||||
|
[]byte(`INSERT INTO "users__temp" ("id", "org_id") SELECT "id", "org_id" FROM "users"`),
|
||||||
|
[]byte(`DROP TABLE IF EXISTS "users"`),
|
||||||
|
[]byte(`ALTER TABLE "users__temp" RENAME TO "users"`),
|
||||||
|
// Note that a unique index is created because a unique constraint already existed for the table.
|
||||||
|
[]byte(`CREATE UNIQUE INDEX IF NOT EXISTS "uq_users_id" ON "users" ("id")`),
|
||||||
|
},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ForeignKeyConstraint_DoesNotExist_DropConstraintFalse",
|
||||||
|
table: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Note that the name of the referencing column is different from the one in the table.
|
||||||
|
constraint: &ForeignKeyConstraint{ReferencingColumnName: "orgid", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
uniqueConstraints: []*UniqueConstraint{
|
||||||
|
{ColumnNames: []ColumnName{"id"}},
|
||||||
|
},
|
||||||
|
support: OperatorSupport{
|
||||||
|
DropConstraint: false,
|
||||||
|
},
|
||||||
|
expectedSQLs: [][]byte{},
|
||||||
|
expectedTable: &Table{
|
||||||
|
Name: "users",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "org_id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "orgs", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fmter := NewFormatter(schema.NewNopFormatter().Dialect())
|
||||||
|
operator := NewOperator(fmter, testCase.support)
|
||||||
|
|
||||||
|
actuals := operator.DropConstraint(testCase.table, testCase.uniqueConstraints, testCase.constraint)
|
||||||
|
assert.Equal(t, testCase.expectedSQLs, actuals)
|
||||||
|
assert.Equal(t, testCase.expectedTable, testCase.table)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
326
pkg/sqlschema/sqlitesqlschema/ddl.go
Normal file
326
pkg/sqlschema/sqlitesqlschema/ddl.go
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
package sqlitesqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Inspired by https://github.com/go-gorm/sqlite
|
||||||
|
|
||||||
|
var (
|
||||||
|
sqliteSeparator = "`|\"|'|\t"
|
||||||
|
sqliteIdentQuote = "`|\"|'"
|
||||||
|
uniqueRegexp = regexp.MustCompile(fmt.Sprintf(`^(?:CONSTRAINT [%v]?[\w-]+[%v]? )?UNIQUE (.*)$`, sqliteSeparator, sqliteSeparator))
|
||||||
|
tableRegexp = regexp.MustCompile(fmt.Sprintf(`(?is)(CREATE TABLE [%v]?[\w\d-]+[%v]?)(?:\s*\((.*)\))?`, sqliteSeparator, sqliteSeparator))
|
||||||
|
tableNameRegexp = regexp.MustCompile(fmt.Sprintf(`CREATE TABLE [%v]?([\w-]+)[%v]?`, sqliteSeparator, sqliteSeparator))
|
||||||
|
checkRegexp = regexp.MustCompile(`^(?i)CHECK[\s]*\(`)
|
||||||
|
constraintRegexp = regexp.MustCompile(fmt.Sprintf(`CONSTRAINT\s+[%v]?[\w\d_]+[%v]?[\s]+`, sqliteSeparator, sqliteSeparator))
|
||||||
|
foreignKeyRegexp = regexp.MustCompile(fmt.Sprintf(`FOREIGN KEY\s*\(\s*[%v]?(\w+)[%v]?\s*\)\s*REFERENCES\s*[%v]?(\w+)[%v]?\s*\(\s*[%v]?(\w+)[%v]?\s*\)`, sqliteSeparator, sqliteSeparator, sqliteSeparator, sqliteSeparator, sqliteSeparator, sqliteSeparator))
|
||||||
|
referencesRegexp = regexp.MustCompile(fmt.Sprintf(`(\w+)\s*(\w+)\s*REFERENCES\s*[%v]?(\w+)[%v]?\s*\(\s*[%v]?(\w+)[%v]?\s*\)`, sqliteSeparator, sqliteSeparator, sqliteSeparator, sqliteSeparator))
|
||||||
|
identQuoteRegexp = regexp.MustCompile(fmt.Sprintf("[%v]", sqliteIdentQuote))
|
||||||
|
columnRegexp = regexp.MustCompile(fmt.Sprintf(`^[%v]?([\w\d]+)[%v]?\s+([\w\(\)\d]+)(.*)$`, sqliteSeparator, sqliteSeparator))
|
||||||
|
defaultValueRegexp = regexp.MustCompile(`(?i) DEFAULT \(?(.+)?\)?( |COLLATE|GENERATED|$)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type parseAllColumnsState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
parseAllColumnsState_NONE parseAllColumnsState = iota
|
||||||
|
parseAllColumnsState_Beginning
|
||||||
|
parseAllColumnsState_ReadingRawName
|
||||||
|
parseAllColumnsState_ReadingQuotedName
|
||||||
|
parseAllColumnsState_EndOfName
|
||||||
|
parseAllColumnsState_State_End
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseCreateTable(str string, fmter sqlschema.SQLFormatter) (*sqlschema.Table, []*sqlschema.UniqueConstraint, error) {
|
||||||
|
sections := tableRegexp.FindStringSubmatch(str)
|
||||||
|
if len(sections) == 0 {
|
||||||
|
return nil, nil, errors.New("invalid DDL")
|
||||||
|
}
|
||||||
|
|
||||||
|
tableNameSections := tableNameRegexp.FindStringSubmatch(str)
|
||||||
|
if len(tableNameSections) == 0 {
|
||||||
|
return nil, nil, errors.New("invalid DDL")
|
||||||
|
}
|
||||||
|
|
||||||
|
tableName := sqlschema.TableName(tableNameSections[1])
|
||||||
|
fields := make([]string, 0)
|
||||||
|
columns := make([]*sqlschema.Column, 0)
|
||||||
|
var primaryKeyConstraint *sqlschema.PrimaryKeyConstraint
|
||||||
|
foreignKeyConstraints := make([]*sqlschema.ForeignKeyConstraint, 0)
|
||||||
|
uniqueConstraints := make([]*sqlschema.UniqueConstraint, 0)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ddlBody = sections[2]
|
||||||
|
ddlBodyRunes = []rune(ddlBody)
|
||||||
|
bracketLevel int
|
||||||
|
quote rune
|
||||||
|
buf string
|
||||||
|
)
|
||||||
|
ddlBodyRunesLen := len(ddlBodyRunes)
|
||||||
|
|
||||||
|
for idx := 0; idx < ddlBodyRunesLen; idx++ {
|
||||||
|
var (
|
||||||
|
next rune = 0
|
||||||
|
c = ddlBodyRunes[idx]
|
||||||
|
)
|
||||||
|
if idx+1 < ddlBodyRunesLen {
|
||||||
|
next = ddlBodyRunes[idx+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if sc := string(c); identQuoteRegexp.MatchString(sc) {
|
||||||
|
if c == next {
|
||||||
|
buf += sc // Skip escaped quote
|
||||||
|
idx++
|
||||||
|
} else if quote > 0 {
|
||||||
|
quote = 0
|
||||||
|
} else {
|
||||||
|
quote = c
|
||||||
|
}
|
||||||
|
} else if quote == 0 {
|
||||||
|
if c == '(' {
|
||||||
|
bracketLevel++
|
||||||
|
} else if c == ')' {
|
||||||
|
bracketLevel--
|
||||||
|
} else if bracketLevel == 0 {
|
||||||
|
if c == ',' {
|
||||||
|
fields = append(fields, strings.TrimSpace(buf))
|
||||||
|
buf = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bracketLevel < 0 {
|
||||||
|
return nil, nil, errors.New("invalid DDL, unbalanced brackets")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf += string(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bracketLevel != 0 {
|
||||||
|
return nil, nil, errors.New("invalid DDL, unbalanced brackets")
|
||||||
|
}
|
||||||
|
|
||||||
|
if buf != "" {
|
||||||
|
fields = append(fields, strings.TrimSpace(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range fields {
|
||||||
|
fUpper := strings.ToUpper(f)
|
||||||
|
if checkRegexp.MatchString(f) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(fUpper, "FOREIGN KEY") {
|
||||||
|
matches := foreignKeyRegexp.FindStringSubmatch(f)
|
||||||
|
if len(matches) >= 4 {
|
||||||
|
foreignKeyConstraints = append(foreignKeyConstraints, &sqlschema.ForeignKeyConstraint{
|
||||||
|
ReferencingColumnName: sqlschema.ColumnName(matches[1]),
|
||||||
|
ReferencedTableName: sqlschema.TableName(matches[2]),
|
||||||
|
ReferencedColumnName: sqlschema.ColumnName(matches[3]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can never be a column name, so we can skip it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(fUpper, "REFERENCES") && !strings.Contains(fUpper, "FOREIGN KEY") {
|
||||||
|
matches := referencesRegexp.FindStringSubmatch(f)
|
||||||
|
if len(matches) >= 4 {
|
||||||
|
foreignKeyConstraints = append(foreignKeyConstraints, &sqlschema.ForeignKeyConstraint{
|
||||||
|
ReferencingColumnName: sqlschema.ColumnName(matches[1]),
|
||||||
|
ReferencedTableName: sqlschema.TableName(matches[3]),
|
||||||
|
ReferencedColumnName: sqlschema.ColumnName(matches[4]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match unique constraints
|
||||||
|
if matches := uniqueRegexp.FindStringSubmatch(f); matches != nil {
|
||||||
|
if len(matches) > 0 {
|
||||||
|
cols, err := parseAllColumns(matches[1])
|
||||||
|
if err == nil {
|
||||||
|
uniqueConstraints = append(uniqueConstraints, &sqlschema.UniqueConstraint{
|
||||||
|
ColumnNames: cols,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This can never be a column name, so we can skip it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches := constraintRegexp.FindStringSubmatch(f); len(matches) > 0 {
|
||||||
|
if strings.Contains(fUpper, "PRIMARY KEY") {
|
||||||
|
cols, err := parseAllColumns(f)
|
||||||
|
if err == nil {
|
||||||
|
primaryKeyConstraint = &sqlschema.PrimaryKeyConstraint{
|
||||||
|
ColumnNames: cols,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can never be a column name, so we can skip it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(fUpper, "PRIMARY KEY") {
|
||||||
|
cols, err := parseAllColumns(f)
|
||||||
|
if err == nil {
|
||||||
|
primaryKeyConstraint = &sqlschema.PrimaryKeyConstraint{
|
||||||
|
ColumnNames: cols,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if matches := columnRegexp.FindStringSubmatch(f); len(matches) > 0 {
|
||||||
|
column := &sqlschema.Column{
|
||||||
|
Name: sqlschema.ColumnName(matches[1]),
|
||||||
|
DataType: fmter.DataTypeOf(matches[2]),
|
||||||
|
Nullable: true,
|
||||||
|
Default: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
matchUpper := strings.ToUpper(matches[3])
|
||||||
|
if strings.Contains(matchUpper, " NOT NULL") {
|
||||||
|
column.Nullable = false
|
||||||
|
} else if strings.Contains(matchUpper, " NULL") {
|
||||||
|
column.Nullable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(matchUpper, " UNIQUE") && !strings.Contains(matchUpper, " PRIMARY") {
|
||||||
|
uniqueConstraints = append(uniqueConstraints, &sqlschema.UniqueConstraint{
|
||||||
|
ColumnNames: []sqlschema.ColumnName{column.Name},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(matchUpper, " PRIMARY") {
|
||||||
|
column.Nullable = false
|
||||||
|
primaryKeyConstraint = &sqlschema.PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []sqlschema.ColumnName{column.Name},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if defaultMatches := defaultValueRegexp.FindStringSubmatch(matches[3]); len(defaultMatches) > 1 {
|
||||||
|
if strings.ToLower(defaultMatches[1]) != "null" {
|
||||||
|
column.Default = strings.Trim(defaultMatches[1], `"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
columns = append(columns, column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sqlschema.Table{
|
||||||
|
Name: tableName,
|
||||||
|
Columns: columns,
|
||||||
|
PrimaryKeyConstraint: primaryKeyConstraint,
|
||||||
|
ForeignKeyConstraints: foreignKeyConstraints,
|
||||||
|
}, uniqueConstraints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAllColumns(in string) ([]sqlschema.ColumnName, error) {
|
||||||
|
s := []rune(in)
|
||||||
|
columns := make([]sqlschema.ColumnName, 0)
|
||||||
|
state := parseAllColumnsState_NONE
|
||||||
|
quote := rune(0)
|
||||||
|
name := make([]rune, 0)
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
switch state {
|
||||||
|
case parseAllColumnsState_NONE:
|
||||||
|
if s[i] == '(' {
|
||||||
|
state = parseAllColumnsState_Beginning
|
||||||
|
}
|
||||||
|
case parseAllColumnsState_Beginning:
|
||||||
|
if isSpace(s[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isQuote(s[i]) {
|
||||||
|
state = parseAllColumnsState_ReadingQuotedName
|
||||||
|
quote = s[i]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s[i] == '[' {
|
||||||
|
state = parseAllColumnsState_ReadingQuotedName
|
||||||
|
quote = ']'
|
||||||
|
continue
|
||||||
|
} else if s[i] == ')' {
|
||||||
|
return columns, fmt.Errorf("unexpected token: %s", string(s[i]))
|
||||||
|
}
|
||||||
|
state = parseAllColumnsState_ReadingRawName
|
||||||
|
name = append(name, s[i])
|
||||||
|
case parseAllColumnsState_ReadingRawName:
|
||||||
|
if isSeparator(s[i]) {
|
||||||
|
state = parseAllColumnsState_Beginning
|
||||||
|
columns = append(columns, sqlschema.ColumnName(name))
|
||||||
|
name = make([]rune, 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s[i] == ')' {
|
||||||
|
state = parseAllColumnsState_State_End
|
||||||
|
columns = append(columns, sqlschema.ColumnName(name))
|
||||||
|
}
|
||||||
|
if isQuote(s[i]) {
|
||||||
|
return nil, fmt.Errorf("unexpected token: %s", string(s[i]))
|
||||||
|
}
|
||||||
|
if isSpace(s[i]) {
|
||||||
|
state = parseAllColumnsState_EndOfName
|
||||||
|
columns = append(columns, sqlschema.ColumnName(name))
|
||||||
|
name = make([]rune, 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name = append(name, s[i])
|
||||||
|
case parseAllColumnsState_ReadingQuotedName:
|
||||||
|
if s[i] == quote {
|
||||||
|
// check if quote character is escaped
|
||||||
|
if i+1 < len(s) && s[i+1] == quote {
|
||||||
|
name = append(name, quote)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
state = parseAllColumnsState_EndOfName
|
||||||
|
columns = append(columns, sqlschema.ColumnName(name))
|
||||||
|
name = make([]rune, 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name = append(name, s[i])
|
||||||
|
case parseAllColumnsState_EndOfName:
|
||||||
|
if isSpace(s[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isSeparator(s[i]) {
|
||||||
|
state = parseAllColumnsState_Beginning
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s[i] == ')' {
|
||||||
|
state = parseAllColumnsState_State_End
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unexpected token: %s", string(s[i]))
|
||||||
|
case parseAllColumnsState_State_End:
|
||||||
|
// break is automatic in Go switch statements
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if state != parseAllColumnsState_State_End {
|
||||||
|
return nil, errors.New("unexpected end")
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpace(r rune) bool {
|
||||||
|
return r == ' ' || r == '\t'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isQuote(r rune) bool {
|
||||||
|
return r == '`' || r == '"' || r == '\''
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSeparator(r rune) bool {
|
||||||
|
return r == ','
|
||||||
|
}
|
||||||
191
pkg/sqlschema/sqlitesqlschema/ddl_test.go
Normal file
191
pkg/sqlschema/sqlitesqlschema/ddl_test.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package sqlitesqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseCreateTable(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
sql string
|
||||||
|
table *sqlschema.Table
|
||||||
|
uniqueConstraints []*sqlschema.UniqueConstraint
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "NewlineAndTabBeforeComma_NoQuotesInColumnNames_InlineConstraints_References",
|
||||||
|
sql: `CREATE TABLE test (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
data TEXT
|
||||||
|
, created_at TIMESTAMP, updated_at TIMESTAMP, org_id TEXT REFERENCES organizations(id) ON DELETE CASCADE)`,
|
||||||
|
table: &sqlschema.Table{
|
||||||
|
Name: "test",
|
||||||
|
Columns: []*sqlschema.Column{
|
||||||
|
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "email", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "data", DataType: sqlschema.DataTypeText, Nullable: true},
|
||||||
|
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: true},
|
||||||
|
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: true},
|
||||||
|
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: true},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
|
||||||
|
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "organizations", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*sqlschema.UniqueConstraint{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SingleLine_QuotesInColumnNames_SeparateConstraints_PrimaryAndForeign",
|
||||||
|
sql: `CREATE TABLE "test" ("id" TEXT NOT NULL, "display_name" TEXT NOT NULL, "org_id" TEXT NOT NULL, CONSTRAINT "pk_users" PRIMARY KEY ("id"), CONSTRAINT "fk_users_org_id" FOREIGN KEY ("org_id") REFERENCES "organizations" ("id"))`,
|
||||||
|
table: &sqlschema.Table{
|
||||||
|
Name: "test",
|
||||||
|
Columns: []*sqlschema.Column{
|
||||||
|
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "display_name", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
|
||||||
|
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "organizations", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*sqlschema.UniqueConstraint{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SingleLine_QuotesInColumnNames_InlineConstraints_UniqueAndForeign",
|
||||||
|
sql: `CREATE TABLE "test" ("id" text NOT NULL, "created_at" TIMESTAMP, "org_id" text NOT NULL, PRIMARY KEY ("id"), UNIQUE ("org_id"), FOREIGN KEY ("org_id") REFERENCES "organizations" ("id"))`,
|
||||||
|
table: &sqlschema.Table{
|
||||||
|
Name: "test",
|
||||||
|
Columns: []*sqlschema.Column{
|
||||||
|
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: true},
|
||||||
|
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
|
||||||
|
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "organizations", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*sqlschema.UniqueConstraint{
|
||||||
|
{ColumnNames: []sqlschema.ColumnName{"org_id"}},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SingleLine_NoQuotes_InlineConstraints_2ColumnsInUnique",
|
||||||
|
sql: `CREATE TABLE "test" ("id" text NOT NULL, "signal" TEXT NOT NULL, "org_id" text NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "org_id_signal" UNIQUE ("org_id", "signal"))`,
|
||||||
|
table: &sqlschema.Table{
|
||||||
|
Name: "test",
|
||||||
|
Columns: []*sqlschema.Column{
|
||||||
|
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "signal", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*sqlschema.UniqueConstraint{
|
||||||
|
{ColumnNames: []sqlschema.ColumnName{"org_id", "signal"}},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tabbed_BacktickQuotes_Constraints_PrimaryAndUnique",
|
||||||
|
sql: "CREATE TABLE `test` (id integer primary key unique, dark_mode numeric DEFAULT true)",
|
||||||
|
table: &sqlschema.Table{
|
||||||
|
Name: "test",
|
||||||
|
Columns: []*sqlschema.Column{
|
||||||
|
{Name: "id", DataType: sqlschema.DataTypeInteger, Nullable: false},
|
||||||
|
{Name: "dark_mode", DataType: sqlschema.DataTypeNumeric, Nullable: true, Default: "true"},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*sqlschema.UniqueConstraint{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SingleLine_BacktickQuotesInteger_NoConstraints",
|
||||||
|
sql: "CREATE TABLE `test-hyphen` (`field` integer NOT NULL)",
|
||||||
|
table: &sqlschema.Table{
|
||||||
|
Name: "test-hyphen",
|
||||||
|
Columns: []*sqlschema.Column{
|
||||||
|
{Name: "field", DataType: sqlschema.DataTypeInteger, Nullable: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*sqlschema.UniqueConstraint{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SingleLine_BacktickQuotesNumeric_NoConstraints",
|
||||||
|
sql: "CREATE TABLE `test-hyphen` (`field` real NOT NULL)",
|
||||||
|
table: &sqlschema.Table{
|
||||||
|
Name: "test-hyphen",
|
||||||
|
Columns: []*sqlschema.Column{
|
||||||
|
{Name: "field", DataType: sqlschema.DataTypeNumeric, Nullable: false},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*sqlschema.UniqueConstraint{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SingleLine_QuotesAndDefaultInColumnNames_2Constraints_UniqueAndForeign",
|
||||||
|
sql: `CREATE TABLE "test" ("id" text NOT NULL, "created_at" TIMESTAMP, "updated_at" TIMESTAMP, "status" text NOT NULL DEFAULT 'notstarted', "org_id" text NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "idx" UNIQUE ("org_id", "status", "created_at"), FOREIGN KEY ("org_id") REFERENCES "organizations" ("id"))`,
|
||||||
|
table: &sqlschema.Table{
|
||||||
|
Name: "test",
|
||||||
|
Columns: []*sqlschema.Column{
|
||||||
|
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: true},
|
||||||
|
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: true},
|
||||||
|
{Name: "status", DataType: sqlschema.DataTypeText, Nullable: false, Default: "'notstarted'"},
|
||||||
|
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{ColumnNames: []sqlschema.ColumnName{"id"}},
|
||||||
|
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "organizations", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*sqlschema.UniqueConstraint{
|
||||||
|
{ColumnNames: []sqlschema.ColumnName{"org_id", "status", "created_at"}},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SingleLine_QuotesAndDefaultInColumnNames_NoConstraints",
|
||||||
|
sql: `CREATE TABLE "real_default" ("id" INTEGER NOT NULL, "r" REAL DEFAULT 1.0)`,
|
||||||
|
table: &sqlschema.Table{
|
||||||
|
Name: "real_default",
|
||||||
|
Columns: []*sqlschema.Column{
|
||||||
|
{Name: "id", DataType: sqlschema.DataTypeInteger, Nullable: false},
|
||||||
|
{Name: "r", DataType: sqlschema.DataTypeNumeric, Nullable: true, Default: "1.0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
uniqueConstraints: []*sqlschema.UniqueConstraint{},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
table, uniqueConstraints, err := parseCreateTable(testCase.sql, Formatter{sqlschema.NewFormatter(sqlitedialect.New())})
|
||||||
|
if testCase.err != nil {
|
||||||
|
assert.Equal(t, testCase.err, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.table.Name, table.Name)
|
||||||
|
assert.ElementsMatch(t, testCase.table.Columns, table.Columns)
|
||||||
|
assert.Equal(t, testCase.table.PrimaryKeyConstraint, table.PrimaryKeyConstraint)
|
||||||
|
assert.ElementsMatch(t, testCase.table.ForeignKeyConstraints, table.ForeignKeyConstraints)
|
||||||
|
assert.ElementsMatch(t, testCase.uniqueConstraints, uniqueConstraints)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
28
pkg/sqlschema/sqlitesqlschema/formatter.go
Normal file
28
pkg/sqlschema/sqlitesqlschema/formatter.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package sqlitesqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Formatter struct {
|
||||||
|
sqlschema.Formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter Formatter) SQLDataTypeOf(dataType sqlschema.DataType) string {
|
||||||
|
if dataType == sqlschema.DataTypeNumeric {
|
||||||
|
return "REAL"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToUpper(dataType.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (formatter Formatter) DataTypeOf(dataType string) sqlschema.DataType {
|
||||||
|
switch strings.ToUpper(dataType) {
|
||||||
|
case "REAL":
|
||||||
|
return sqlschema.DataTypeNumeric
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter.Formatter.DataTypeOf(dataType)
|
||||||
|
}
|
||||||
144
pkg/sqlschema/sqlitesqlschema/provider.go
Normal file
144
pkg/sqlschema/sqlitesqlschema/provider.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package sqlitesqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
settings factory.ScopedProviderSettings
|
||||||
|
fmter sqlschema.SQLFormatter
|
||||||
|
sqlstore sqlstore.SQLStore
|
||||||
|
operator sqlschema.SQLOperator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("sqlite"), func(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config) (sqlschema.SQLSchema, error) {
|
||||||
|
return New(ctx, providerSettings, config, sqlstore)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, providerSettings factory.ProviderSettings, config sqlschema.Config, sqlstore sqlstore.SQLStore) (sqlschema.SQLSchema, error) {
|
||||||
|
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/sqlschema/sqlitesqlschema")
|
||||||
|
fmter := Formatter{sqlschema.NewFormatter(sqlstore.BunDB().Dialect())}
|
||||||
|
|
||||||
|
return &provider{
|
||||||
|
fmter: fmter,
|
||||||
|
settings: settings,
|
||||||
|
sqlstore: sqlstore,
|
||||||
|
operator: sqlschema.NewOperator(fmter, sqlschema.OperatorSupport{
|
||||||
|
DropConstraint: false,
|
||||||
|
ColumnIfNotExistsExists: false,
|
||||||
|
AlterColumnSetNotNull: false,
|
||||||
|
}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Formatter() sqlschema.SQLFormatter {
|
||||||
|
return provider.fmter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Operator() sqlschema.SQLOperator {
|
||||||
|
return provider.operator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetTable(ctx context.Context, tableName sqlschema.TableName) (*sqlschema.Table, []*sqlschema.UniqueConstraint, error) {
|
||||||
|
var sql string
|
||||||
|
|
||||||
|
if err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewRaw("SELECT sql FROM sqlite_master WHERE type IN (?) AND tbl_name = ? AND sql IS NOT NULL", bun.In([]string{"table"}), string(tableName)).
|
||||||
|
Scan(ctx, &sql); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
table, uniqueConstraints, err := parseCreateTable(sql, provider.fmter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return table, uniqueConstraints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetIndices(ctx context.Context, tableName sqlschema.TableName) ([]sqlschema.Index, error) {
|
||||||
|
rows, err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
QueryContext(ctx, "SELECT * FROM PRAGMA_index_list(?)", string(tableName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "error closing rows", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
indices := []sqlschema.Index{}
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
seq int
|
||||||
|
name string
|
||||||
|
unique bool
|
||||||
|
origin string
|
||||||
|
partial bool
|
||||||
|
columns []sqlschema.ColumnName
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&seq, &name, &unique, &origin, &partial); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip the index that was created by a UNIQUE constraint
|
||||||
|
if origin == "u" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip the index that was created by primary key constraint
|
||||||
|
if origin == "pk" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewRaw("SELECT name FROM PRAGMA_index_info(?)", string(name)).
|
||||||
|
Scan(ctx, &columns); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if unique {
|
||||||
|
indices = append(indices, (&sqlschema.UniqueIndex{
|
||||||
|
TableName: tableName,
|
||||||
|
ColumnNames: columns,
|
||||||
|
}).Named(name).(*sqlschema.UniqueIndex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return indices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) ToggleFKEnforcement(ctx context.Context, db bun.IDB, on bool) error {
|
||||||
|
_, err := db.ExecContext(ctx, "PRAGMA foreign_keys = ?", on)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var val bool
|
||||||
|
if err := db.NewRaw("PRAGMA foreign_keys").Scan(ctx, &val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if on == val {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.NewInternalf(errors.CodeInternal, "foreign_keys(actual: %s, expected: %s), maybe a transaction is in progress?", strconv.FormatBool(val), strconv.FormatBool(on))
|
||||||
|
}
|
||||||
69
pkg/sqlschema/sqlschema.go
Normal file
69
pkg/sqlschema/sqlschema.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SQLSchema interface {
|
||||||
|
// Returns the formatter for the schema.
|
||||||
|
Formatter() SQLFormatter
|
||||||
|
|
||||||
|
// Returns the operator for the schema.
|
||||||
|
Operator() SQLOperator
|
||||||
|
|
||||||
|
// Inspects the schema and returns the table with the given name.
|
||||||
|
GetTable(context.Context, TableName) (*Table, []*UniqueConstraint, error)
|
||||||
|
|
||||||
|
// Inspects the schema and returns the indices for the given table.
|
||||||
|
GetIndices(context.Context, TableName) ([]Index, error)
|
||||||
|
|
||||||
|
// Toggles foreign key enforcement for the schema for the current session.
|
||||||
|
ToggleFKEnforcement(context.Context, bun.IDB, bool) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLOperator performs operations on a table.
|
||||||
|
type SQLOperator interface {
|
||||||
|
// Returns a list of SQL statements to create a table.
|
||||||
|
CreateTable(*Table) [][]byte
|
||||||
|
|
||||||
|
// Returns a list of SQL statements to drop a table.
|
||||||
|
DropTable(*Table) [][]byte
|
||||||
|
|
||||||
|
// Returns a list of SQL statements to rename a table.
|
||||||
|
RenameTable(*Table, TableName) [][]byte
|
||||||
|
|
||||||
|
// Returns a list of SQL statements to recreate a table.
|
||||||
|
RecreateTable(*Table, []*UniqueConstraint) [][]byte
|
||||||
|
|
||||||
|
// Returns a list of SQL statements to create an index.
|
||||||
|
CreateIndex(Index) [][]byte
|
||||||
|
|
||||||
|
// Returns a list of SQL statements to drop an index.
|
||||||
|
DropIndex(Index) [][]byte
|
||||||
|
|
||||||
|
// Returns a list of SQL statements to add a column to a table.
|
||||||
|
// If the column is not nullable, the column is added with the input value, then the column is made non-nullable.
|
||||||
|
AddColumn(*Table, []*UniqueConstraint, *Column, any) [][]byte
|
||||||
|
|
||||||
|
// Returns a list of SQL statements to drop a column from a table.
|
||||||
|
DropColumn(*Table, *Column) [][]byte
|
||||||
|
|
||||||
|
// Returns a list of SQL statements to drop a constraint from a table.
|
||||||
|
DropConstraint(*Table, []*UniqueConstraint, Constraint) [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type SQLFormatter interface {
|
||||||
|
// Returns the SQL data type for the given data type.
|
||||||
|
SQLDataTypeOf(DataType) string
|
||||||
|
|
||||||
|
// Returns the data type for the given SQL data type.
|
||||||
|
DataTypeOf(string) DataType
|
||||||
|
|
||||||
|
// Appends an identifier to the given byte slice.
|
||||||
|
AppendIdent([]byte, string) []byte
|
||||||
|
|
||||||
|
// Appends a value to the given byte slice.
|
||||||
|
AppendValue([]byte, any) []byte
|
||||||
|
}
|
||||||
58
pkg/sqlschema/sqlschematest/provider.go
Normal file
58
pkg/sqlschema/sqlschematest/provider.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package sqlschematest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlschema"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ sqlschema.SQLSchema = (*Provider)(nil)
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
Fmter sqlschema.Formatter
|
||||||
|
Tables map[string]*sqlschema.Table
|
||||||
|
UniqueConstraints map[string][]*sqlschema.UniqueConstraint
|
||||||
|
Indices map[string]sqlschema.Index
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(tables map[string]*sqlschema.Table, uniqueConstraints map[string][]*sqlschema.UniqueConstraint, indices map[string]sqlschema.Index) *Provider {
|
||||||
|
return &Provider{
|
||||||
|
Fmter: sqlschema.NewFormatter(schema.NewNopFormatter().Dialect()),
|
||||||
|
Tables: tables,
|
||||||
|
UniqueConstraints: uniqueConstraints,
|
||||||
|
Indices: indices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) Formatter() sqlschema.SQLFormatter {
|
||||||
|
return provider.Fmter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) Operator() sqlschema.SQLOperator {
|
||||||
|
return sqlschema.NewOperator(provider.Fmter, sqlschema.OperatorSupport{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) GetTable(ctx context.Context, name sqlschema.TableName) (*sqlschema.Table, []*sqlschema.UniqueConstraint, error) {
|
||||||
|
table, ok := provider.Tables[string(name)]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errors.NewNotFoundf(errors.CodeNotFound, "table %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return table, provider.UniqueConstraints[string(name)], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) GetIndices(ctx context.Context, name sqlschema.TableName) ([]sqlschema.Index, error) {
|
||||||
|
indices, ok := provider.Indices[string(name)]
|
||||||
|
if !ok {
|
||||||
|
return []sqlschema.Index{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []sqlschema.Index{indices}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Provider) ToggleFKEnforcement(_ context.Context, _ bun.IDB, _ bool) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
156
pkg/sqlschema/table.go
Normal file
156
pkg/sqlschema/table.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
type TableName string
|
||||||
|
|
||||||
|
type Table struct {
|
||||||
|
// The name of the table.
|
||||||
|
Name TableName
|
||||||
|
|
||||||
|
// The columns that the table contains.
|
||||||
|
Columns []*Column
|
||||||
|
|
||||||
|
// The primary key constraint that the table contains.
|
||||||
|
PrimaryKeyConstraint *PrimaryKeyConstraint
|
||||||
|
|
||||||
|
// The foreign key constraints that the table contains.
|
||||||
|
ForeignKeyConstraints []*ForeignKeyConstraint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *Table) Clone() *Table {
|
||||||
|
copyOfColumns := make([]*Column, len(table.Columns))
|
||||||
|
copy(copyOfColumns, table.Columns)
|
||||||
|
|
||||||
|
copyOfForeignKeyConstraints := make([]*ForeignKeyConstraint, len(table.ForeignKeyConstraints))
|
||||||
|
copy(copyOfForeignKeyConstraints, table.ForeignKeyConstraints)
|
||||||
|
|
||||||
|
return &Table{
|
||||||
|
Name: table.Name,
|
||||||
|
Columns: copyOfColumns,
|
||||||
|
PrimaryKeyConstraint: table.PrimaryKeyConstraint,
|
||||||
|
ForeignKeyConstraints: copyOfForeignKeyConstraints,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *Table) DropConstraint(constraint Constraint) (Constraint, bool) {
|
||||||
|
var droppedConstraint Constraint
|
||||||
|
found := false
|
||||||
|
|
||||||
|
if table.PrimaryKeyConstraint != nil && constraint.Equals(table.PrimaryKeyConstraint) {
|
||||||
|
droppedConstraint = table.PrimaryKeyConstraint
|
||||||
|
table.PrimaryKeyConstraint = nil
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if constraint.Type() == ConstraintTypeForeignKey {
|
||||||
|
for i, fkConstraint := range table.ForeignKeyConstraints {
|
||||||
|
if constraint.Equals(fkConstraint) {
|
||||||
|
droppedConstraint = fkConstraint
|
||||||
|
table.ForeignKeyConstraints = append(table.ForeignKeyConstraints[:i], table.ForeignKeyConstraints[i+1:]...)
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return droppedConstraint, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *Table) ToDropSQL(fmter SQLFormatter) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "DROP TABLE IF EXISTS "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(table.Name))
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *Table) ToRenameSQL(fmter SQLFormatter, newName TableName) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "ALTER TABLE "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(table.Name))
|
||||||
|
sql = append(sql, " RENAME TO "...)
|
||||||
|
sql = fmter.AppendIdent(sql, string(newName))
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a temporary table with the same schema as the input table,
|
||||||
|
// inserts the data from the input table into the temporary table, drops the input table,
|
||||||
|
// and then renames the temporary table to the input table name.
|
||||||
|
//
|
||||||
|
// It creates constraints with the same name as the input table making it unfit for RDMS systems which will complain about duplicate constraints.
|
||||||
|
// It is only useful for SQLite.
|
||||||
|
func (table *Table) ToCreateTempInsertDropAlterSQL(fmter SQLFormatter) [][]byte {
|
||||||
|
sql := [][]byte{}
|
||||||
|
|
||||||
|
tempTable := table.Clone()
|
||||||
|
tempTable.Name = table.Name + "__temp"
|
||||||
|
|
||||||
|
if tempTable.PrimaryKeyConstraint != nil {
|
||||||
|
tempTable.PrimaryKeyConstraint = tempTable.PrimaryKeyConstraint.Named(table.PrimaryKeyConstraint.Name(table.Name)).(*PrimaryKeyConstraint)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, constraint := range tempTable.ForeignKeyConstraints {
|
||||||
|
tempTable.ForeignKeyConstraints[i] = constraint.Named(constraint.Name(table.Name)).(*ForeignKeyConstraint)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = append(sql, tempTable.ToCreateSQL(fmter))
|
||||||
|
|
||||||
|
columns := []byte{}
|
||||||
|
for i, column := range table.Columns {
|
||||||
|
if i > 0 {
|
||||||
|
columns = append(columns, ", "...)
|
||||||
|
}
|
||||||
|
|
||||||
|
columns = fmter.AppendIdent(columns, string(column.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
insertIntoSelectSQL := []byte{}
|
||||||
|
insertIntoSelectSQL = append(insertIntoSelectSQL, "INSERT INTO "...)
|
||||||
|
insertIntoSelectSQL = fmter.AppendIdent(insertIntoSelectSQL, string(tempTable.Name))
|
||||||
|
insertIntoSelectSQL = append(insertIntoSelectSQL, " ("...)
|
||||||
|
|
||||||
|
insertIntoSelectSQL = append(insertIntoSelectSQL, columns...)
|
||||||
|
insertIntoSelectSQL = append(insertIntoSelectSQL, ") SELECT "...)
|
||||||
|
insertIntoSelectSQL = append(insertIntoSelectSQL, columns...)
|
||||||
|
insertIntoSelectSQL = append(insertIntoSelectSQL, " FROM "...)
|
||||||
|
insertIntoSelectSQL = fmter.AppendIdent(insertIntoSelectSQL, string(table.Name))
|
||||||
|
|
||||||
|
sql = append(sql, insertIntoSelectSQL)
|
||||||
|
sql = append(sql, table.ToDropSQL(fmter))
|
||||||
|
sql = append(sql, tempTable.ToRenameSQL(fmter, table.Name))
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *Table) ToCreateSQL(fmter SQLFormatter) []byte {
|
||||||
|
sql := []byte{}
|
||||||
|
|
||||||
|
sql = append(sql, "CREATE TABLE IF NOT EXISTS "...)
|
||||||
|
|
||||||
|
sql = fmter.AppendIdent(sql, string(table.Name))
|
||||||
|
sql = append(sql, " ("...)
|
||||||
|
|
||||||
|
for i, column := range table.Columns {
|
||||||
|
if i > 0 {
|
||||||
|
sql = append(sql, ", "...)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = append(sql, column.ToDefinitionSQL(fmter)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if table.PrimaryKeyConstraint != nil {
|
||||||
|
sql = append(sql, ", "...)
|
||||||
|
sql = append(sql, table.PrimaryKeyConstraint.ToDefinitionSQL(fmter, table.Name)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, constraint := range table.ForeignKeyConstraints {
|
||||||
|
sql = append(sql, ", "...)
|
||||||
|
sql = append(sql, constraint.ToDefinitionSQL(fmter, table.Name)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = append(sql, ")"...)
|
||||||
|
|
||||||
|
return sql
|
||||||
|
}
|
||||||
177
pkg/sqlschema/table_test.go
Normal file
177
pkg/sqlschema/table_test.go
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
package sqlschema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/uptrace/bun/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableToCreateSQL(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
table *Table
|
||||||
|
sql string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "NoPrimaryKey_NoForeignKey_Nullable_BooleanDefault",
|
||||||
|
table: &Table{
|
||||||
|
Name: "boolean_default",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "b", DataType: DataTypeBoolean, Nullable: true, Default: "false"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS "boolean_default" ("id" INTEGER NOT NULL, "b" BOOLEAN DEFAULT false)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NoPrimaryKey_NoForeignKey_Nullable_TextDefault",
|
||||||
|
table: &Table{
|
||||||
|
Name: "text_default",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "t", DataType: DataTypeText, Nullable: true, Default: "'text'"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS "text_default" ("id" INTEGER NOT NULL, "t" TEXT DEFAULT 'text')`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NoPrimaryKey_NoForeignKey_Nullable_IntegerDefault",
|
||||||
|
table: &Table{
|
||||||
|
Name: "integer_default",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "i", DataType: DataTypeInteger, Nullable: true, Default: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS "integer_default" ("id" INTEGER NOT NULL, "i" INTEGER DEFAULT 1)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NoPrimaryKey_NoForeignKey_Nullable_NumericDefault",
|
||||||
|
table: &Table{
|
||||||
|
Name: "numeric_default",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "n", DataType: DataTypeNumeric, Nullable: true, Default: "1.0"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS "numeric_default" ("id" INTEGER NOT NULL, "n" NUMERIC DEFAULT 1.0)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NoPrimaryKey_NoForeignKey_Nullable_TimestampDefault",
|
||||||
|
table: &Table{
|
||||||
|
Name: "timestamp_default",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeInteger, Nullable: false, Default: ""},
|
||||||
|
{Name: "t", DataType: DataTypeTimestamp, Nullable: true, Default: "CURRENT_TIMESTAMP"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS "timestamp_default" ("id" INTEGER NOT NULL, "t" TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PrimaryKey_NonNullable",
|
||||||
|
table: &Table{
|
||||||
|
Name: "test",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeText, Nullable: false},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false},
|
||||||
|
{Name: "org_id", DataType: DataTypeText, Nullable: false},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS "test" ("id" TEXT NOT NULL, "name" TEXT NOT NULL, "org_id" TEXT NOT NULL, CONSTRAINT "pk_test" PRIMARY KEY ("id"))`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PrimaryKey_ForeignKey_NonNullable",
|
||||||
|
table: &Table{
|
||||||
|
Name: "test",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeText, Nullable: false},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false},
|
||||||
|
{Name: "org_id", DataType: DataTypeText, Nullable: false},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &PrimaryKeyConstraint{ColumnNames: []ColumnName{"id"}},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "organizations", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS "test" ("id" TEXT NOT NULL, "name" TEXT NOT NULL, "org_id" TEXT NOT NULL, CONSTRAINT "pk_test" PRIMARY KEY ("id"), CONSTRAINT "fk_test_org_id" FOREIGN KEY ("org_id") REFERENCES "organizations" ("id"))`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PrimaryKey_MultipleForeignKeys_NonNullable",
|
||||||
|
table: &Table{
|
||||||
|
Name: "test",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeText, Nullable: false},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false},
|
||||||
|
{Name: "org_id", DataType: DataTypeText, Nullable: false},
|
||||||
|
{Name: "user_id", DataType: DataTypeText, Nullable: false},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &PrimaryKeyConstraint{ColumnNames: []ColumnName{"id"}},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{ReferencingColumnName: "org_id", ReferencedTableName: "organizations", ReferencedColumnName: "id"},
|
||||||
|
{ReferencingColumnName: "user_id", ReferencedTableName: "users", ReferencedColumnName: "id"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sql: `CREATE TABLE IF NOT EXISTS "test" ("id" TEXT NOT NULL, "name" TEXT NOT NULL, "org_id" TEXT NOT NULL, "user_id" TEXT NOT NULL, CONSTRAINT "pk_test" PRIMARY KEY ("id"), CONSTRAINT "fk_test_org_id" FOREIGN KEY ("org_id") REFERENCES "organizations" ("id"), CONSTRAINT "fk_test_user_id" FOREIGN KEY ("user_id") REFERENCES "users" ("id"))`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fmter := Formatter{schema.NewNopFormatter()}
|
||||||
|
sql := testCase.table.ToCreateSQL(fmter)
|
||||||
|
|
||||||
|
assert.Equal(t, testCase.sql, string(sql))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTableToCreateTempInsertDropAlterSQL(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
table Table
|
||||||
|
sqls []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "PrimaryKey_ForeignKey_NonNullable",
|
||||||
|
table: Table{
|
||||||
|
Name: "test",
|
||||||
|
Columns: []*Column{
|
||||||
|
{Name: "id", DataType: DataTypeText, Nullable: false},
|
||||||
|
{Name: "name", DataType: DataTypeText, Nullable: false},
|
||||||
|
{Name: "org_id", DataType: DataTypeText, Nullable: false},
|
||||||
|
},
|
||||||
|
PrimaryKeyConstraint: &PrimaryKeyConstraint{
|
||||||
|
ColumnNames: []ColumnName{"id"},
|
||||||
|
},
|
||||||
|
ForeignKeyConstraints: []*ForeignKeyConstraint{
|
||||||
|
{
|
||||||
|
ReferencingColumnName: "org_id",
|
||||||
|
ReferencedTableName: "organizations",
|
||||||
|
ReferencedColumnName: "id",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sqls: []string{
|
||||||
|
`CREATE TABLE IF NOT EXISTS "test__temp" ("id" TEXT NOT NULL, "name" TEXT NOT NULL, "org_id" TEXT NOT NULL, CONSTRAINT "pk_test" PRIMARY KEY ("id"), CONSTRAINT "fk_test_org_id" FOREIGN KEY ("org_id") REFERENCES "organizations" ("id"))`,
|
||||||
|
`INSERT INTO "test__temp" ("id", "name", "org_id") SELECT "id", "name", "org_id" FROM "test"`,
|
||||||
|
`DROP TABLE IF EXISTS "test"`,
|
||||||
|
`ALTER TABLE "test__temp" RENAME TO "test"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
fmter := Formatter{schema.NewNopFormatter()}
|
||||||
|
sqls := testCase.table.ToCreateTempInsertDropAlterSQL(fmter)
|
||||||
|
|
||||||
|
for i, sql := range sqls {
|
||||||
|
assert.Equal(t, testCase.sqls[i], string(sql))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user