diff --git a/Taskfile.yml b/Taskfile.yml index 8e1d139..b90a579 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -145,14 +145,27 @@ tasks: desc: Generate Go code from all schema examples. dir: "{{.git_root}}" cmds: - - rm -rf tests/localstack/generated - - mkdir -p tests/localstack/generated - - | - for schema in tests/fixtures/*.json; do + - rm -rf tests/localstack/generated + - mkdir -p tests/localstack/generated + - | + for schema in tests/fixtures/*.json; do + filename=$(basename "${schema}" .json) + + if [[ "${filename}" == *__* ]]; then + mode="${filename##*__}" + echo "Generating with mode: ${mode} for ${schema}" + + go run ./cmd/dyno generate \ + -schema "${schema}" \ + -output-dir "tests/localstack/generated" \ + -mode "${mode}" + else + echo "Generating without mode for ${schema}" go run ./cmd/dyno generate \ -schema "${schema}" \ -output-dir "tests/localstack/generated" - done + fi + done silent: true internal: true diff --git a/internal/app/commands/generate/action.go b/internal/app/commands/generate/action.go index 8dcc01e..4bb7cf7 100644 --- a/internal/app/commands/generate/action.go +++ b/internal/app/commands/generate/action.go @@ -5,6 +5,7 @@ import ( "github.com/Mad-Pixels/go-dyno/internal/app/flags" "github.com/Mad-Pixels/go-dyno/internal/generator" + "github.com/Mad-Pixels/go-dyno/internal/generator/mode" "github.com/Mad-Pixels/go-dyno/internal/logger" "github.com/Mad-Pixels/go-dyno/internal/utils/conv" "github.com/Mad-Pixels/go-dyno/internal/utils/writer" @@ -16,10 +17,18 @@ func action(ctx *cli.Context) error { var ( schemaPath = ctx.String(flags.LocalSchema.GetName()) outputPath = ctx.String(flags.LocalOutputDir.GetName()) + modeRaw = ctx.String(flags.LocalGenerateMode.GetName()) ) + + m, err := mode.ParseMode(modeRaw) + if err != nil { + return err + } + logger.Log.Debug(). Str("schema", schemaPath). Str("output", outputPath). + Str("mode", m.String()). Msg("Starting code generation") g, err := generator.NewGenerator(schemaPath) @@ -30,7 +39,8 @@ func action(ctx *cli.Context) error { return err } - builder := g.NewRenderBuilder() + builder := g.NewRenderBuilder(). + WithMode(m) if ctx.IsSet(flags.LocalPackageName.GetName()) { var ( raw = ctx.String(flags.LocalPackageName.GetName()) diff --git a/internal/app/commands/generate/command.go b/internal/app/commands/generate/command.go index 9b23914..cf863ce 100644 --- a/internal/app/commands/generate/command.go +++ b/internal/app/commands/generate/command.go @@ -19,6 +19,7 @@ type tmplUsage struct { EnvPrefix string FlagSchemaPath string + FlagMode string } // Command entrypoint. @@ -30,6 +31,7 @@ func Command() *cli.Command { EnvPrefix: godyno.EnvPrefix, FlagSchemaPath: flags.LocalSchema.GetName(), + FlagMode: flags.LocalGenerateMode.GetName(), }, ) @@ -44,6 +46,7 @@ func Command() *cli.Command { flags.LocalOutputDir.Object, flags.LocalFilename.Object, flags.LocalPackageName.Object, + flags.LocalGenerateMode.Object, }, } } diff --git a/internal/app/flags/local.go b/internal/app/flags/local.go index 0fd4f47..8f70590 100644 --- a/internal/app/flags/local.go +++ b/internal/app/flags/local.go @@ -5,6 +5,7 @@ import ( "strings" godyno "github.com/Mad-Pixels/go-dyno" + "github.com/Mad-Pixels/go-dyno/internal/generator/mode" "github.com/urfave/cli/v2" ) @@ -68,4 +69,21 @@ var ( Required: false, }, } + + // LocalGenerateMode defines the --mode flag for controlling code generation mode. + // Determines what code to generate: ALL (complete) or MIN (minimal). + LocalGenerateMode = Flag{ + Object: &cli.StringFlag{ + Name: "mode", + Usage: fmt.Sprintf("Set generation mode (%s). (default: %s)", strings.Join(mode.GetAvailableModes(), ", "), mode.GetDefault()), + Aliases: []string{ + "m", + }, + EnvVars: []string{ + fmt.Sprintf("%s_%s", godyno.EnvPrefix, strings.ToUpper("mode")), + }, + Required: false, + Value: mode.GetDefault().String(), + }, + } ) diff --git a/internal/generator/builder.go b/internal/generator/builder.go index a9737d0..38aaa25 100644 --- a/internal/generator/builder.go +++ b/internal/generator/builder.go @@ -1,6 +1,7 @@ package generator import ( + "github.com/Mad-Pixels/go-dyno/internal/generator/mode" "github.com/Mad-Pixels/go-dyno/internal/logger" "github.com/Mad-Pixels/go-dyno/internal/utils/conv" "github.com/Mad-Pixels/go-dyno/internal/utils/tmpl" @@ -11,6 +12,7 @@ import ( // Allows overriding schema defaults (package name, filename) via CLI flags. type RenderBuilder struct { generator *Generator + mode *mode.Mode packageName *string filename *string } @@ -33,6 +35,14 @@ func (rb *RenderBuilder) WithFilename(name string) *RenderBuilder { return rb } +// WithMode overrides the generator mode type. +func (rb *RenderBuilder) WithMode(mode mode.Mode) *RenderBuilder { + if mode.IsValid() { + rb.mode = &mode + } + return rb +} + // Build renders the final Go code using configured overrides. func (rb *RenderBuilder) Build() string { var ( @@ -60,6 +70,14 @@ func (rb *RenderBuilder) GetFilename() string { return rb.generator.schema.Filename() } +// GetMode returns the current generation mode (or default if not set). +func (rb *RenderBuilder) GetMode() mode.Mode { + if rb.mode != nil { + return *rb.mode + } + return mode.GetDefault() +} + // buildTemplateMap creates template data with schema and overrides. func (rb *RenderBuilder) buildTemplateMap() v2.TemplateMap { schema := rb.generator.schema @@ -69,6 +87,7 @@ func (rb *RenderBuilder) buildTemplateMap() v2.TemplateMap { TableName: schema.TableName(), HashKey: schema.HashKey(), RangeKey: schema.RangeKey(), + Mode: rb.GetMode(), Attributes: schema.Attributes(), CommonAttributes: schema.CommonAttributes(), AllAttributes: schema.AllAttributes(), diff --git a/internal/generator/mode/mode.go b/internal/generator/mode/mode.go new file mode 100644 index 0000000..73ff161 --- /dev/null +++ b/internal/generator/mode/mode.go @@ -0,0 +1,91 @@ +// Package mode defines the generation modes for code generation. +// +// It provides a type-safe enum for controlling what code is generated, +// allowing users to choose between generating all code (ALL) or just +// the minimum required code (MIN). +package mode + +import ( + "strings" + + "github.com/Mad-Pixels/go-dyno/internal/logger" + "github.com/Mad-Pixels/go-dyno/internal/utils/conv" +) + +// Mode represents the code generation mode. +type Mode string + +const ( + // ALL generates complete code with all features (default). + ALL Mode = "ALL" + + // MIN generates minimal code with only essential functionality. + MIN Mode = "MIN" +) + +// Valid modes for validation. +var validModes = map[Mode]bool{ + ALL: true, + MIN: true, +} + +// String returns the string representation of the Mode. +func (m Mode) String() string { + return string(m) +} + +// IsValid checks if the mode is a valid generation mode. +func (m Mode) IsValid() bool { + return validModes[m] +} + +// ParseMode parses a string into a Mode type with case-insensitive matching. +// Returns the parsed mode and an error if the string is not a valid mode. +func ParseMode(s string) (Mode, error) { + mode := Mode(strings.ToUpper(strings.TrimSpace(s))) + if !mode.IsValid() { + return "", logger.NewFailure("invalid generation mode", nil). + With("mode", s). + With("available", GetAvailableModes()) + } + return mode, nil +} + +// MustParseMode parses a string into a Mode type and panics on error. +// Should only be used in tests or when the input is guaranteed to be valid. +func MustParseMode(s string) Mode { + mode, err := ParseMode(s) + if err != nil { + panic(err) + } + return mode +} + +// GetDefault returns the default generation mode. +func GetDefault() Mode { + return ALL +} + +// GetAvailableModes returns a slice of all valid modes sorted alphabetically. +func GetAvailableModes() []string { + stringModes := make(map[string]bool, len(validModes)) + for mode := range validModes { + stringModes[string(mode)] = true + } + return conv.AvailableKeys(stringModes) +} + +// IsALL checks if the given mode is ALL. +func IsALL(m Mode) bool { + return m == ALL +} + +// IsMIN checks if the given mode is MIN. +func IsMIN(m Mode) bool { + return m == MIN +} + +// IsMode checks if the given mode matches the target mode string. +func IsMode(m Mode, target string) bool { + return m.String() == target +} diff --git a/internal/utils/tmpl/tmpl.go b/internal/utils/tmpl/tmpl.go index a4d4ab3..396ab1b 100644 --- a/internal/utils/tmpl/tmpl.go +++ b/internal/utils/tmpl/tmpl.go @@ -19,6 +19,7 @@ import ( "text/template" "github.com/Mad-Pixels/go-dyno/internal/generator/attribute" + "github.com/Mad-Pixels/go-dyno/internal/generator/mode" "github.com/Mad-Pixels/go-dyno/internal/logger" "github.com/Mad-Pixels/go-dyno/internal/utils/conv" "github.com/rs/zerolog" @@ -152,6 +153,9 @@ func renderTemplate(b *bytes.Buffer, tmpl string, vars any, shouldFormat bool) { "GetUsedNumericSetTypes": attribute.GetUsedNumericSetTypes, "IsFloatType": conv.IsFloatType, "Slice": conv.TrimLeftN, + "IsALL": mode.IsALL, + "IsMIN": mode.IsMIN, + "IsMode": mode.IsMode, }, ). Parse(tmpl) diff --git a/templates/v2/core/mixin_filter.go b/templates/v2/core/mixin_filter.go new file mode 100644 index 0000000..3b81800 --- /dev/null +++ b/templates/v2/core/mixin_filter.go @@ -0,0 +1,39 @@ +package core + +// KeyConditionMixinSugarTemplate provides convenience With methods (only for ALL mode) +const KeyConditionMixinSugarTemplate = ` +// CONVENIENCE METHODS - Only available in ALL mode + +// WithEQ adds equality key condition. +// Required for partition key, optional for sort key. +// Example: .WithEQ("user_id", "123") +func (kcm *KeyConditionMixin) WithEQ(field string, value any) { + kcm.With(field, EQ, value) +} + +// WithBetween adds range key condition for sort keys. +// Example: .WithBetween("created_at", start_time, end_time) +func (kcm *KeyConditionMixin) WithBetween(field string, start, end any) { + kcm.With(field, BETWEEN, start, end) +} + +// WithGT adds greater than key condition for sort keys. +func (kcm *KeyConditionMixin) WithGT(field string, value any) { + kcm.With(field, GT, value) +} + +// WithGTE adds greater than or equal key condition for sort keys. +func (kcm *KeyConditionMixin) WithGTE(field string, value any) { + kcm.With(field, GTE, value) +} + +// WithLT adds less than key condition for sort keys. +func (kcm *KeyConditionMixin) WithLT(field string, value any) { + kcm.With(field, LT, value) +} + +// WithLTE adds less than or equal key condition for sort keys. +func (kcm *KeyConditionMixin) WithLTE(field string, value any) { + kcm.With(field, LTE, value) +} +` diff --git a/templates/v2/core/mixins.go b/templates/v2/core/mixins.go index 8789614..131ac72 100644 --- a/templates/v2/core/mixins.go +++ b/templates/v2/core/mixins.go @@ -42,90 +42,6 @@ func (fm *FilterMixin) Filter(field string, op OperatorType, values ...any) { } } -// FilterEQ adds equality filter condition. -// Example: .FilterEQ("status", "active") -func (fm *FilterMixin) FilterEQ(field string, value any) { - fm.Filter(field, EQ, value) -} - -// FilterContains adds contains filter for strings or sets. -// Example: .FilterContains("tags", "important") -func (fm *FilterMixin) FilterContains(field string, value any) { - fm.Filter(field, CONTAINS, value) -} - -// FilterNotContains adds not contains filter for strings or sets. -func (fm *FilterMixin) FilterNotContains(field string, value any) { - fm.Filter(field, NOT_CONTAINS, value) -} - -// FilterBeginsWith adds begins_with filter for strings. -// Example: .FilterBeginsWith("email", "admin@") -func (fm *FilterMixin) FilterBeginsWith(field string, value any) { - fm.Filter(field, BEGINS_WITH, value) -} - -// FilterBetween adds range filter for comparable values. -// Example: .FilterBetween("price", 10, 100) -func (fm *FilterMixin) FilterBetween(field string, start, end any) { - fm.Filter(field, BETWEEN, start, end) -} - -// FilterGT adds greater than filter. -func (fm *FilterMixin) FilterGT(field string, value any) { - fm.Filter(field, GT, value) -} - -// FilterLT adds less than filter. -func (fm *FilterMixin) FilterLT(field string, value any) { - fm.Filter(field, LT, value) -} - -// FilterGTE adds greater than or equal filter. -func (fm *FilterMixin) FilterGTE(field string, value any) { - fm.Filter(field, GTE, value) -} - -// FilterLTE adds less than or equal filter. -func (fm *FilterMixin) FilterLTE(field string, value any) { - fm.Filter(field, LTE, value) -} - -// FilterExists checks if attribute exists. -// Example: .FilterExists("optional_field") -func (fm *FilterMixin) FilterExists(field string) { - fm.Filter(field, EXISTS) -} - -// FilterNotExists checks if attribute does not exist. -func (fm *FilterMixin) FilterNotExists(field string) { - fm.Filter(field, NOT_EXISTS) -} - -// FilterNE adds not equal filter. -func (fm *FilterMixin) FilterNE(field string, value any) { - fm.Filter(field, NE, value) -} - -// FilterIn adds IN filter for scalar values. -// For DynamoDB Sets (SS/NS), use FilterContains instead. -// Example: .FilterIn("category", "books", "electronics") -func (fm *FilterMixin) FilterIn(field string, values ...any) { - if len(values) == 0 { - return - } - fm.Filter(field, IN, values...) -} - -// FilterNotIn adds NOT_IN filter for scalar values. -// For DynamoDB Sets (SS/NS), use FilterNotContains instead. -func (fm *FilterMixin) FilterNotIn(field string, values ...any) { - if len(values) == 0 { - return - } - fm.Filter(field, NOT_IN, values...) -} - // PaginationMixin provides pagination support for Query and Scan operations. type PaginationMixin struct { LimitValue *int @@ -190,39 +106,6 @@ func (kcm *KeyConditionMixin) With(field string, op OperatorType, values ...any) kcm.KeyConditions[field] = keyCond } -// WithEQ adds equality key condition. -// Required for partition key, optional for sort key. -// Example: .WithEQ("user_id", "123") -func (kcm *KeyConditionMixin) WithEQ(field string, value any) { - kcm.With(field, EQ, value) -} - -// WithBetween adds range key condition for sort keys. -// Example: .WithBetween("created_at", start_time, end_time) -func (kcm *KeyConditionMixin) WithBetween(field string, start, end any) { - kcm.With(field, BETWEEN, start, end) -} - -// WithGT adds greater than key condition for sort keys. -func (kcm *KeyConditionMixin) WithGT(field string, value any) { - kcm.With(field, GT, value) -} - -// WithGTE adds greater than or equal key condition for sort keys. -func (kcm *KeyConditionMixin) WithGTE(field string, value any) { - kcm.With(field, GTE, value) -} - -// WithLT adds less than key condition for sort keys. -func (kcm *KeyConditionMixin) WithLT(field string, value any) { - kcm.With(field, LT, value) -} - -// WithLTE adds less than or equal key condition for sort keys. -func (kcm *KeyConditionMixin) WithLTE(field string, value any) { - kcm.With(field, LTE, value) -} - // WithPreferredSortKey sets preferred sort key for index selection. // Useful when multiple indexes match the query pattern. func (kcm *KeyConditionMixin) WithPreferredSortKey(key string) { diff --git a/templates/v2/core/mixins_with.go b/templates/v2/core/mixins_with.go new file mode 100644 index 0000000..bd64729 --- /dev/null +++ b/templates/v2/core/mixins_with.go @@ -0,0 +1,90 @@ +package core + +// FilterMixinSugarTemplate provides convenience Filter methods (only for ALL mode) +const FilterMixinSugarTemplate = ` +// CONVENIENCE METHODS - Only available in ALL mode + +// FilterEQ adds equality filter condition. +// Example: .FilterEQ("status", "active") +func (fm *FilterMixin) FilterEQ(field string, value any) { + fm.Filter(field, EQ, value) +} + +// FilterContains adds contains filter for strings or sets. +// Example: .FilterContains("tags", "important") +func (fm *FilterMixin) FilterContains(field string, value any) { + fm.Filter(field, CONTAINS, value) +} + +// FilterNotContains adds not contains filter for strings or sets. +func (fm *FilterMixin) FilterNotContains(field string, value any) { + fm.Filter(field, NOT_CONTAINS, value) +} + +// FilterBeginsWith adds begins_with filter for strings. +// Example: .FilterBeginsWith("email", "admin@") +func (fm *FilterMixin) FilterBeginsWith(field string, value any) { + fm.Filter(field, BEGINS_WITH, value) +} + +// FilterBetween adds range filter for comparable values. +// Example: .FilterBetween("price", 10, 100) +func (fm *FilterMixin) FilterBetween(field string, start, end any) { + fm.Filter(field, BETWEEN, start, end) +} + +// FilterGT adds greater than filter. +func (fm *FilterMixin) FilterGT(field string, value any) { + fm.Filter(field, GT, value) +} + +// FilterLT adds less than filter. +func (fm *FilterMixin) FilterLT(field string, value any) { + fm.Filter(field, LT, value) +} + +// FilterGTE adds greater than or equal filter. +func (fm *FilterMixin) FilterGTE(field string, value any) { + fm.Filter(field, GTE, value) +} + +// FilterLTE adds less than or equal filter. +func (fm *FilterMixin) FilterLTE(field string, value any) { + fm.Filter(field, LTE, value) +} + +// FilterExists checks if attribute exists. +// Example: .FilterExists("optional_field") +func (fm *FilterMixin) FilterExists(field string) { + fm.Filter(field, EXISTS) +} + +// FilterNotExists checks if attribute does not exist. +func (fm *FilterMixin) FilterNotExists(field string) { + fm.Filter(field, NOT_EXISTS) +} + +// FilterNE adds not equal filter. +func (fm *FilterMixin) FilterNE(field string, value any) { + fm.Filter(field, NE, value) +} + +// FilterIn adds IN filter for scalar values. +// For DynamoDB Sets (SS/NS), use FilterContains instead. +// Example: .FilterIn("category", "books", "electronics") +func (fm *FilterMixin) FilterIn(field string, values ...any) { + if len(values) == 0 { + return + } + fm.Filter(field, IN, values...) +} + +// FilterNotIn adds NOT_IN filter for scalar values. +// For DynamoDB Sets (SS/NS), use FilterNotContains instead. +func (fm *FilterMixin) FilterNotIn(field string, values ...any) { + if len(values) == 0 { + return + } + fm.Filter(field, NOT_IN, values...) +} +` diff --git a/templates/v2/query/build.go b/templates/v2/query/build.go index 4d54a09..4efd458 100644 --- a/templates/v2/query/build.go +++ b/templates/v2/query/build.go @@ -59,6 +59,7 @@ func (qb *QueryBuilder) Build() (string, expression.KeyConditionBuilder, *expres } } var filterConditions []expression.ConditionBuilder + filterConditions = append(filterConditions, qb.FilterConditions...) for attrName, value := range qb.Attributes { if attrName != TableSchema.HashKey && attrName != TableSchema.RangeKey { filterConditions = append(filterConditions, expression.Name(attrName).Equal(expression.Value(value))) @@ -135,6 +136,8 @@ func (qb *QueryBuilder) buildRangeKeyCondition(idx SecondaryIndex) (*expression. // Moves non-key conditions to filter expressions for optimal query performance. func (qb *QueryBuilder) buildFilterCondition(idx SecondaryIndex) *expression.ConditionBuilder { var filterConditions []expression.ConditionBuilder + + filterConditions = append(filterConditions, qb.FilterConditions...) for attrName, value := range qb.Attributes { if qb.isPartOfIndexKey(attrName, idx) { continue diff --git a/templates/v2/query/builder.go b/templates/v2/query/builder.go index 42fbe4e..4687ba8 100644 --- a/templates/v2/query/builder.go +++ b/templates/v2/query/builder.go @@ -1,6 +1,6 @@ package query -// QueryBuilderTemplate provides the main QueryBuilder struct with universal index methods +// QueryBuilderTemplate provides the main QueryBuilder struct and basic methods const QueryBuilderTemplate = ` // QueryBuilder provides a fluent interface for building type-safe DynamoDB queries. // Combines FilterMixin, PaginationMixin, and KeyConditionMixin for comprehensive query building. @@ -23,213 +23,6 @@ func NewQueryBuilder() *QueryBuilder { } } -// Filter adds a filter condition and returns QueryBuilder for method chaining. -// Wraps FilterMixin.Filter with fluent interface support. -func (qb *QueryBuilder) Filter(field string, op OperatorType, values ...any) *QueryBuilder { - qb.FilterMixin.Filter(field, op, values...) - return qb -} - -// FilterEQ adds equality filter and returns QueryBuilder for method chaining. -// Example: query.FilterEQ("status", "active") -func (qb *QueryBuilder) FilterEQ(field string, value any) *QueryBuilder { - qb.FilterMixin.FilterEQ(field, value) - return qb -} - -// FilterContains adds contains filter and returns QueryBuilder for method chaining. -// Works with String attributes (substring) and Set attributes (membership). -// Example: query.FilterContains("tags", "premium") -func (qb *QueryBuilder) FilterContains(field string, value any) *QueryBuilder { - qb.FilterMixin.FilterContains(field, value) - return qb -} - -// FilterNotContains adds not contains filter and returns QueryBuilder for method chaining. -// Opposite of FilterContains for exclusion filtering. -func (qb *QueryBuilder) FilterNotContains(field string, value any) *QueryBuilder { - qb.FilterMixin.FilterNotContains(field, value) - return qb -} - -// FilterBeginsWith adds begins_with filter and returns QueryBuilder for method chaining. -// Only works with String attributes for prefix matching. -// Example: query.FilterBeginsWith("email", "admin@") -func (qb *QueryBuilder) FilterBeginsWith(field string, value any) *QueryBuilder { - qb.FilterMixin.FilterBeginsWith(field, value) - return qb -} - -// FilterBetween adds range filter and returns QueryBuilder for method chaining. -// Works with comparable types for inclusive range filtering. -// Example: query.FilterBetween("score", 80, 100) -func (qb *QueryBuilder) FilterBetween(field string, start, end any) *QueryBuilder { - qb.FilterMixin.FilterBetween(field, start, end) - return qb -} - -// FilterGT adds greater than filter and returns QueryBuilder for method chaining. -// Example: query.FilterGT("last_login", cutoffDate) -func (qb *QueryBuilder) FilterGT(field string, value any) *QueryBuilder { - qb.FilterMixin.FilterGT(field, value) - return qb -} - -// FilterLT adds less than filter and returns QueryBuilder for method chaining. -// Example: query.FilterLT("attempts", maxAttempts) -func (qb *QueryBuilder) FilterLT(field string, value any) *QueryBuilder { - qb.FilterMixin.FilterLT(field, value) - return qb -} - -// FilterGTE adds greater than or equal filter and returns QueryBuilder for method chaining. -// Example: query.FilterGTE("age", minimumAge) -func (qb *QueryBuilder) FilterGTE(field string, value any) *QueryBuilder { - qb.FilterMixin.FilterGTE(field, value) - return qb -} - -// FilterLTE adds less than or equal filter and returns QueryBuilder for method chaining. -// Example: query.FilterLTE("file_size", maxFileSize) -func (qb *QueryBuilder) FilterLTE(field string, value any) *QueryBuilder { - qb.FilterMixin.FilterLTE(field, value) - return qb -} - -// FilterExists adds attribute exists filter and returns QueryBuilder for method chaining. -// Checks if the specified attribute exists in the item. -// Example: query.FilterExists("optional_field") -func (qb *QueryBuilder) FilterExists(field string) *QueryBuilder { - qb.FilterMixin.FilterExists(field) - return qb -} - -// FilterNotExists adds attribute not exists filter and returns QueryBuilder for method chaining. -// Checks if the specified attribute does not exist in the item. -func (qb *QueryBuilder) FilterNotExists(field string) *QueryBuilder { - qb.FilterMixin.FilterNotExists(field) - return qb -} - -// FilterNE adds not equal filter and returns QueryBuilder for method chaining. -// Example: query.FilterNE("status", "deleted") -func (qb *QueryBuilder) FilterNE(field string, value any) *QueryBuilder { - qb.FilterMixin.FilterNE(field, value) - return qb -} - -// FilterIn adds IN filter and returns QueryBuilder for method chaining. -// For scalar values only - use FilterContains for DynamoDB Sets. -// Example: query.FilterIn("category", "books", "electronics", "clothing") -func (qb *QueryBuilder) FilterIn(field string, values ...any) *QueryBuilder { - qb.FilterMixin.FilterIn(field, values...) - return qb -} - -// FilterNotIn adds NOT_IN filter and returns QueryBuilder for method chaining. -// For scalar values only - use FilterNotContains for DynamoDB Sets. -func (qb *QueryBuilder) FilterNotIn(field string, values ...any) *QueryBuilder { - qb.FilterMixin.FilterNotIn(field, values...) - return qb -} - -// With adds key condition and returns QueryBuilder for method chaining. -// Only works with partition and sort key attributes for efficient querying. -// Example: query.With("user_id", EQ, "123").With("created_at", GT, timestamp) -func (qb *QueryBuilder) With(field string, op OperatorType, values ...any) *QueryBuilder { - qb.KeyConditionMixin.With(field, op, values...) - if op == EQ && len(values) == 1 { - qb.Attributes[field] = values[0] - qb.UsedKeys[field] = true - } - return qb -} - -// WithEQ adds equality key condition and returns QueryBuilder for method chaining. -// Required for partition keys, commonly used for sort keys. -// Example: query.WithEQ("user_id", "123") -func (qb *QueryBuilder) WithEQ(field string, value any) *QueryBuilder { - qb.KeyConditionMixin.WithEQ(field, value) - qb.Attributes[field] = value - qb.UsedKeys[field] = true - return qb -} - -// WithBetween adds range key condition and returns QueryBuilder for method chaining. -// Only valid for sort keys, not partition keys. -// Example: query.WithBetween("timestamp", startTime, endTime) -func (qb *QueryBuilder) WithBetween(field string, start, end any) *QueryBuilder { - qb.KeyConditionMixin.WithBetween(field, start, end) - qb.Attributes[field+"_start"] = start - qb.Attributes[field+"_end"] = end - qb.UsedKeys[field] = true - return qb -} - -// WithGT adds greater than key condition and returns QueryBuilder for method chaining. -// Only valid for sort keys in range queries. -// Example: query.WithGT("created_at", yesterday) -func (qb *QueryBuilder) WithGT(field string, value any) *QueryBuilder { - qb.KeyConditionMixin.WithGT(field, value) - qb.Attributes[field] = value - qb.UsedKeys[field] = true - return qb -} - -// WithGTE adds greater than or equal key condition and returns QueryBuilder for method chaining. -// Only valid for sort keys in range queries. -// Example: query.WithGTE("score", minimumScore) -func (qb *QueryBuilder) WithGTE(field string, value any) *QueryBuilder { - qb.KeyConditionMixin.WithGTE(field, value) - qb.Attributes[field] = value - qb.UsedKeys[field] = true - return qb -} - -// WithLT adds less than key condition and returns QueryBuilder for method chaining. -// Only valid for sort keys in range queries. -// Example: query.WithLT("expiry_date", now) -func (qb *QueryBuilder) WithLT(field string, value any) *QueryBuilder { - qb.KeyConditionMixin.WithLT(field, value) - qb.Attributes[field] = value - qb.UsedKeys[field] = true - return qb -} - -// WithLTE adds less than or equal key condition and returns QueryBuilder for method chaining. -// Only valid for sort keys in range queries. -// Example: query.WithLTE("price", maxBudget) -func (qb *QueryBuilder) WithLTE(field string, value any) *QueryBuilder { - qb.KeyConditionMixin.WithLTE(field, value) - qb.Attributes[field] = value - qb.UsedKeys[field] = true - return qb -} - -// WithPreferredSortKey sets the preferred sort key and returns QueryBuilder for method chaining. -// Hints the index selection algorithm when multiple indexes could satisfy the query. -// Example: query.WithPreferredSortKey("created_at") -func (qb *QueryBuilder) WithPreferredSortKey(key string) *QueryBuilder { - qb.KeyConditionMixin.WithPreferredSortKey(key) - return qb -} - -// OrderByDesc sets descending sort order and returns QueryBuilder for method chaining. -// Only affects sort key ordering, not filter results. -// Example: query.OrderByDesc() // newest first -func (qb *QueryBuilder) OrderByDesc() *QueryBuilder { - qb.KeyConditionMixin.OrderByDesc() - return qb -} - -// OrderByAsc sets ascending sort order and returns QueryBuilder for method chaining. -// This is the default sort order. -// Example: query.OrderByAsc() // oldest first -func (qb *QueryBuilder) OrderByAsc() *QueryBuilder { - qb.KeyConditionMixin.OrderByAsc() - return qb -} - // Limit sets the maximum number of items and returns QueryBuilder for method chaining. // Controls the number of items returned in a single request. // Example: query.Limit(25) @@ -246,102 +39,27 @@ func (qb *QueryBuilder) StartFrom(lastEvaluatedKey map[string]types.AttributeVal return qb } -// WithIndexHashKey sets hash key for any index by name. -// Automatically handles both simple and composite keys based on schema metadata. -// For composite keys, pass values in the order they appear in the schema. -// Example: query.WithIndexHashKey("user-status-index", "user123") -// Example: query.WithIndexHashKey("tenant-user-index", "tenant1", "user123") // composite -func (qb *QueryBuilder) WithIndexHashKey(indexName string, values ...any) *QueryBuilder { - index := qb.getIndexByName(indexName) - if index == nil { - return qb - } - - if index.HashKeyParts != nil { - nonConstantParts := qb.getNonConstantParts(index.HashKeyParts) - if len(values) != len(nonConstantParts) { - return qb - } - qb.setCompositeKey(index.HashKey, index.HashKeyParts, values) - } else { - if len(values) != 1 { - return qb - } - qb.Attributes[index.HashKey] = values[0] - qb.UsedKeys[index.HashKey] = true - qb.KeyConditions[index.HashKey] = expression.Key(index.HashKey).Equal(expression.Value(values[0])) - } - return qb -} - -// WithIndexRangeKey sets range key for any index by name. -// Automatically handles both simple and composite keys based on schema metadata. -// For composite keys, pass values in the order they appear in the schema. -// Example: query.WithIndexRangeKey("user-status-index", "active") -// Example: query.WithIndexRangeKey("date-type-index", "2023-01-01", "ORDER") // composite -func (qb *QueryBuilder) WithIndexRangeKey(indexName string, values ...any) *QueryBuilder { - index := qb.getIndexByName(indexName) - if index == nil || index.RangeKey == "" { - return qb - } - - if index.RangeKeyParts != nil { - nonConstantParts := qb.getNonConstantParts(index.RangeKeyParts) - if len(values) != len(nonConstantParts) { - return qb - } - qb.setCompositeKey(index.RangeKey, index.RangeKeyParts, values) - } else { - if len(values) != 1 { - return qb - } - qb.Attributes[index.RangeKey] = values[0] - qb.UsedKeys[index.RangeKey] = true - qb.KeyConditions[index.RangeKey] = expression.Key(index.RangeKey).Equal(expression.Value(values[0])) - } - return qb -} - -// WithIndexRangeKeyBetween sets range key condition for any index with BETWEEN operator. -// Only works with simple range keys, not composite ones. -// Example: query.WithIndexRangeKeyBetween("date-index", startDate, endDate) -func (qb *QueryBuilder) WithIndexRangeKeyBetween(indexName string, start, end any) *QueryBuilder { - index := qb.getIndexByName(indexName) - if index == nil || index.RangeKey == "" || index.RangeKeyParts != nil { - return qb - } - qb.KeyConditions[index.RangeKey] = expression.Key(index.RangeKey).Between(expression.Value(start), expression.Value(end)) - qb.UsedKeys[index.RangeKey] = true - qb.Attributes[index.RangeKey+"_start"] = start - qb.Attributes[index.RangeKey+"_end"] = end +// OrderByDesc sets descending sort order and returns QueryBuilder for method chaining. +// Only affects sort key ordering, not filter results. +// Example: query.OrderByDesc() // newest first +func (qb *QueryBuilder) OrderByDesc() *QueryBuilder { + qb.KeyConditionMixin.OrderByDesc() return qb } -// WithIndexRangeKeyGT sets range key condition for any index with GT operator. -// Only works with simple range keys, not composite ones. -// Example: query.WithIndexRangeKeyGT("score-index", 100) -func (qb *QueryBuilder) WithIndexRangeKeyGT(indexName string, value any) *QueryBuilder { - index := qb.getIndexByName(indexName) - if index == nil || index.RangeKey == "" || index.RangeKeyParts != nil { - return qb - } - qb.KeyConditions[index.RangeKey] = expression.Key(index.RangeKey).GreaterThan(expression.Value(value)) - qb.UsedKeys[index.RangeKey] = true - qb.Attributes[index.RangeKey] = value +// OrderByAsc sets ascending sort order and returns QueryBuilder for method chaining. +// This is the default sort order. +// Example: query.OrderByAsc() // oldest first +func (qb *QueryBuilder) OrderByAsc() *QueryBuilder { + qb.KeyConditionMixin.OrderByAsc() return qb } -// WithIndexRangeKeyLT sets range key condition for any index with LT operator. -// Only works with simple range keys, not composite ones. -// Example: query.WithIndexRangeKeyLT("timestamp-index", cutoffTime) -func (qb *QueryBuilder) WithIndexRangeKeyLT(indexName string, value any) *QueryBuilder { - index := qb.getIndexByName(indexName) - if index == nil || index.RangeKey == "" || index.RangeKeyParts != nil { - return qb - } - qb.KeyConditions[index.RangeKey] = expression.Key(index.RangeKey).LessThan(expression.Value(value)) - qb.UsedKeys[index.RangeKey] = true - qb.Attributes[index.RangeKey] = value +// WithPreferredSortKey sets the preferred sort key and returns QueryBuilder for method chaining. +// Hints the index selection algorithm when multiple indexes could satisfy the query. +// Example: query.WithPreferredSortKey("created_at") +func (qb *QueryBuilder) WithPreferredSortKey(key string) *QueryBuilder { + qb.KeyConditionMixin.WithPreferredSortKey(key) return qb } @@ -377,6 +95,7 @@ func (qb *QueryBuilder) setCompositeKey(keyName string, parts []CompositeKeyPart qb.UsedKeys[part.Value] = true } } + compositeValue := qb.buildCompositeKeyValue(parts) qb.Attributes[keyName] = compositeValue qb.UsedKeys[keyName] = true @@ -399,15 +118,15 @@ func GetIndexInfo(indexName string) *IndexInfo { for _, index := range TableSchema.SecondaryIndexes { if index.Name == indexName { return &IndexInfo{ - Name: index.Name, - Type: getIndexType(index), - HashKey: index.HashKey, - RangeKey: index.RangeKey, - IsHashComposite: len(index.HashKeyParts) > 0, - IsRangeComposite: len(index.RangeKeyParts) > 0, - HashKeyParts: countNonConstantParts(index.HashKeyParts), - RangeKeyParts: countNonConstantParts(index.RangeKeyParts), - ProjectionType: index.ProjectionType, + Name: index.Name, + Type: getIndexType(index), + HashKey: index.HashKey, + RangeKey: index.RangeKey, + IsHashComposite: len(index.HashKeyParts) > 0, + IsRangeComposite: len(index.RangeKeyParts) > 0, + HashKeyParts: countNonConstantParts(index.HashKeyParts), + RangeKeyParts: countNonConstantParts(index.RangeKeyParts), + ProjectionType: index.ProjectionType, } } } @@ -416,24 +135,26 @@ func GetIndexInfo(indexName string) *IndexInfo { // IndexInfo provides metadata about a table index. type IndexInfo struct { - Name string // Index name - Type string // "GSI" or "LSI" - HashKey string // Hash key attribute name - RangeKey string // Range key attribute name (empty if none) - IsHashComposite bool // Whether hash key is composite - IsRangeComposite bool // Whether range key is composite - HashKeyParts int // Number of non-constant hash key parts - RangeKeyParts int // Number of non-constant range key parts - ProjectionType string // "ALL", "KEYS_ONLY", or "INCLUDE" -} - + Name string + Type string + HashKey string + RangeKey string + IsHashComposite bool + IsRangeComposite bool + HashKeyParts int + RangeKeyParts int + ProjectionType string +} + +// getIndexType returns human-readable index type. func getIndexType(index SecondaryIndex) string { - if index.HashKey != TableSchema.HashKey { - return "GSI" + if index.HashKey == "" { + return "LSI" } - return "LSI" + return "GSI" } +// countNonConstantParts counts non-constant parts in composite key. func countNonConstantParts(parts []CompositeKeyPart) int { count := 0 for _, part := range parts { diff --git a/templates/v2/query/builder_filter.go b/templates/v2/query/builder_filter.go new file mode 100644 index 0000000..01927d8 --- /dev/null +++ b/templates/v2/query/builder_filter.go @@ -0,0 +1,119 @@ +package query + +// QueryBuilderFilterTemplate provides Filter methods for query conditions +const QueryBuilderFilterTemplate = ` +// Filter adds a filter condition and returns QueryBuilder for method chaining. +// Wraps FilterMixin.Filter with fluent interface support. +func (qb *QueryBuilder) Filter(field string, op OperatorType, values ...any) *QueryBuilder { + qb.FilterMixin.Filter(field, op, values...) + return qb +} +` + +// QueryBuilderFilterSugarTemplate provides convenience Filter methods (only for ALL mode) +const QueryBuilderFilterSugarTemplate = ` +// CONVENIENCE METHODS - Only available in ALL mode + +// FilterEQ adds equality filter and returns QueryBuilder for method chaining. +// Example: query.FilterEQ("status", "active") +func (qb *QueryBuilder) FilterEQ(field string, value any) *QueryBuilder { + qb.FilterMixin.FilterEQ(field, value) + return qb +} + +// FilterContains adds contains filter and returns QueryBuilder for method chaining. +// Works with String attributes (substring) and Set attributes (membership). +// Example: query.FilterContains("tags", "premium") +func (qb *QueryBuilder) FilterContains(field string, value any) *QueryBuilder { + qb.FilterMixin.FilterContains(field, value) + return qb +} + +// FilterNotContains adds not contains filter and returns QueryBuilder for method chaining. +// Opposite of FilterContains for exclusion filtering. +func (qb *QueryBuilder) FilterNotContains(field string, value any) *QueryBuilder { + qb.FilterMixin.FilterNotContains(field, value) + return qb +} + +// FilterBeginsWith adds begins_with filter and returns QueryBuilder for method chaining. +// Only works with String attributes for prefix matching. +// Example: query.FilterBeginsWith("email", "admin@") +func (qb *QueryBuilder) FilterBeginsWith(field string, value any) *QueryBuilder { + qb.FilterMixin.FilterBeginsWith(field, value) + return qb +} + +// FilterBetween adds range filter and returns QueryBuilder for method chaining. +// Works with comparable types for inclusive range filtering. +// Example: query.FilterBetween("score", 80, 100) +func (qb *QueryBuilder) FilterBetween(field string, start, end any) *QueryBuilder { + qb.FilterMixin.FilterBetween(field, start, end) + return qb +} + +// FilterGT adds greater than filter and returns QueryBuilder for method chaining. +// Example: query.FilterGT("last_login", cutoffDate) +func (qb *QueryBuilder) FilterGT(field string, value any) *QueryBuilder { + qb.FilterMixin.FilterGT(field, value) + return qb +} + +// FilterLT adds less than filter and returns QueryBuilder for method chaining. +// Example: query.FilterLT("attempts", maxAttempts) +func (qb *QueryBuilder) FilterLT(field string, value any) *QueryBuilder { + qb.FilterMixin.FilterLT(field, value) + return qb +} + +// FilterGTE adds greater than or equal filter and returns QueryBuilder for method chaining. +// Example: query.FilterGTE("age", minimumAge) +func (qb *QueryBuilder) FilterGTE(field string, value any) *QueryBuilder { + qb.FilterMixin.FilterGTE(field, value) + return qb +} + +// FilterLTE adds less than or equal filter and returns QueryBuilder for method chaining. +// Example: query.FilterLTE("file_size", maxFileSize) +func (qb *QueryBuilder) FilterLTE(field string, value any) *QueryBuilder { + qb.FilterMixin.FilterLTE(field, value) + return qb +} + +// FilterExists adds attribute exists filter and returns QueryBuilder for method chaining. +// Checks if the specified attribute exists in the item. +// Example: query.FilterExists("optional_field") +func (qb *QueryBuilder) FilterExists(field string) *QueryBuilder { + qb.FilterMixin.FilterExists(field) + return qb +} + +// FilterNotExists adds attribute not exists filter and returns QueryBuilder for method chaining. +// Checks if the specified attribute does not exist in the item. +func (qb *QueryBuilder) FilterNotExists(field string) *QueryBuilder { + qb.FilterMixin.FilterNotExists(field) + return qb +} + +// FilterNE adds not equal filter and returns QueryBuilder for method chaining. +// Example: query.FilterNE("status", "deleted") +func (qb *QueryBuilder) FilterNE(field string, value any) *QueryBuilder { + qb.FilterMixin.FilterNE(field, value) + return qb +} + +// FilterIn adds IN filter and returns QueryBuilder for method chaining. +// For scalar values only - use FilterContains for DynamoDB Sets. +// Example: query.FilterIn("category", "books", "electronics", "clothing") +func (qb *QueryBuilder) FilterIn(field string, values ...any) *QueryBuilder { + qb.FilterMixin.FilterIn(field, values...) + return qb +} + +// FilterNotIn adds NOT_IN filter and returns QueryBuilder for method chaining. +// For scalar values only - use FilterNotContains for DynamoDB Sets. +func (qb *QueryBuilder) FilterNotIn(field string, values ...any) *QueryBuilder { + qb.FilterMixin.FilterNotIn(field, values...) + return qb +} +` diff --git a/templates/v2/query/builder_with.go b/templates/v2/query/builder_with.go new file mode 100644 index 0000000..301d28c --- /dev/null +++ b/templates/v2/query/builder_with.go @@ -0,0 +1,209 @@ +package query + +// QueryBuilderWithTemplate provides With methods for key conditions +const QueryBuilderWithTemplate = ` +// With adds key condition and returns QueryBuilder for method chaining. +// Only works with partition and sort key attributes for efficient querying. +// Example: query.With("user_id", EQ, "123").With("created_at", GT, timestamp) +func (qb *QueryBuilder) With(field string, op OperatorType, values ...any) *QueryBuilder { + qb.KeyConditionMixin.With(field, op, values...) + if op == EQ && len(values) == 1 { + qb.Attributes[field] = values[0] + qb.UsedKeys[field] = true + } + return qb +} +` + +// QueryBuilderWithSugarTemplate provides convenience With methods (only for ALL mode) +const QueryBuilderWithSugarTemplate = ` +// CONVENIENCE METHODS - Only available in ALL mode + +// WithEQ adds equality key condition and returns QueryBuilder for method chaining. +// Required for partition keys, commonly used for sort keys. +// Example: query.WithEQ("user_id", "123") +func (qb *QueryBuilder) WithEQ(field string, value any) *QueryBuilder { + qb.KeyConditionMixin.WithEQ(field, value) + qb.Attributes[field] = value + qb.UsedKeys[field] = true + return qb +} + +// WithBetween adds range key condition and returns QueryBuilder for method chaining. +// Only valid for sort keys, not partition keys. +// Example: query.WithBetween("timestamp", startTime, endTime) +func (qb *QueryBuilder) WithBetween(field string, start, end any) *QueryBuilder { + qb.KeyConditionMixin.WithBetween(field, start, end) + qb.Attributes[field+"_start"] = start + qb.Attributes[field+"_end"] = end + qb.UsedKeys[field] = true + return qb +} + +// WithGT adds greater than key condition and returns QueryBuilder for method chaining. +// Only valid for sort keys in range queries. +// Example: query.WithGT("created_at", yesterday) +func (qb *QueryBuilder) WithGT(field string, value any) *QueryBuilder { + qb.KeyConditionMixin.WithGT(field, value) + qb.Attributes[field] = value + qb.UsedKeys[field] = true + return qb +} + +// WithGTE adds greater than or equal key condition and returns QueryBuilder for method chaining. +// Only valid for sort keys in range queries. +// Example: query.WithGTE("score", minimumScore) +func (qb *QueryBuilder) WithGTE(field string, value any) *QueryBuilder { + qb.KeyConditionMixin.WithGTE(field, value) + qb.Attributes[field] = value + qb.UsedKeys[field] = true + return qb +} + +// WithLT adds less than key condition and returns QueryBuilder for method chaining. +// Only valid for sort keys in range queries. +// Example: query.WithLT("expiry_date", now) +func (qb *QueryBuilder) WithLT(field string, value any) *QueryBuilder { + qb.KeyConditionMixin.WithLT(field, value) + qb.Attributes[field] = value + qb.UsedKeys[field] = true + return qb +} + +// WithLTE adds less than or equal key condition and returns QueryBuilder for method chaining. +// Only valid for sort keys in range queries. +// Example: query.WithLTE("price", maxBudget) +func (qb *QueryBuilder) WithLTE(field string, value any) *QueryBuilder { + qb.KeyConditionMixin.WithLTE(field, value) + qb.Attributes[field] = value + qb.UsedKeys[field] = true + return qb +} + +// WithIndexHashKey sets hash key for any index by name. +// Automatically handles both simple and composite keys based on schema metadata. +// For composite keys, pass values in the order they appear in the schema. +// Example: query.WithIndexHashKey("user-status-index", "user123") +// Example: query.WithIndexHashKey("tenant-user-index", "tenant1", "user123") // composite +func (qb *QueryBuilder) WithIndexHashKey(indexName string, values ...any) *QueryBuilder { + index := qb.getIndexByName(indexName) + if index == nil { + return qb + } + + if index.HashKeyParts != nil { + nonConstantParts := qb.getNonConstantParts(index.HashKeyParts) + if len(values) != len(nonConstantParts) { + return qb + } + qb.setCompositeKey(index.HashKey, index.HashKeyParts, values) + } else { + if len(values) != 1 { + return qb + } + qb.Attributes[index.HashKey] = values[0] + qb.UsedKeys[index.HashKey] = true + qb.KeyConditions[index.HashKey] = expression.Key(index.HashKey).Equal(expression.Value(values[0])) + } + return qb +} + +// WithIndexRangeKey sets range key for any index by name. +// Automatically handles both simple and composite keys based on schema metadata. +// For composite keys, pass values in the order they appear in the schema. +// Example: query.WithIndexRangeKey("user-status-index", "active") +// Example: query.WithIndexRangeKey("date-type-index", "2023-01-01", "ORDER") // composite +func (qb *QueryBuilder) WithIndexRangeKey(indexName string, values ...any) *QueryBuilder { + index := qb.getIndexByName(indexName) + if index == nil || index.RangeKey == "" { + return qb + } + + if index.RangeKeyParts != nil { + nonConstantParts := qb.getNonConstantParts(index.RangeKeyParts) + if len(values) != len(nonConstantParts) { + return qb + } + qb.setCompositeKey(index.RangeKey, index.RangeKeyParts, values) + } else { + if len(values) != 1 { + return qb + } + qb.Attributes[index.RangeKey] = values[0] + qb.UsedKeys[index.RangeKey] = true + qb.KeyConditions[index.RangeKey] = expression.Key(index.RangeKey).Equal(expression.Value(values[0])) + } + return qb +} + +// WithIndexRangeKeyBetween sets range key condition for any index with BETWEEN operator. +// Only works with simple range keys, not composite ones. +// Example: query.WithIndexRangeKeyBetween("date-index", startDate, endDate) +func (qb *QueryBuilder) WithIndexRangeKeyBetween(indexName string, start, end any) *QueryBuilder { + index := qb.getIndexByName(indexName) + if index == nil || index.RangeKey == "" || index.RangeKeyParts != nil { + return qb + } + qb.KeyConditions[index.RangeKey] = expression.Key(index.RangeKey).Between(expression.Value(start), expression.Value(end)) + qb.UsedKeys[index.RangeKey] = true + qb.Attributes[index.RangeKey+"_start"] = start + qb.Attributes[index.RangeKey+"_end"] = end + return qb +} + +// WithIndexRangeKeyGT sets range key condition for any index with GT operator. +// Only works with simple range keys, not composite ones. +// Example: query.WithIndexRangeKeyGT("score-index", 100) +func (qb *QueryBuilder) WithIndexRangeKeyGT(indexName string, value any) *QueryBuilder { + index := qb.getIndexByName(indexName) + if index == nil || index.RangeKey == "" || index.RangeKeyParts != nil { + return qb + } + qb.KeyConditions[index.RangeKey] = expression.Key(index.RangeKey).GreaterThan(expression.Value(value)) + qb.UsedKeys[index.RangeKey] = true + qb.Attributes[index.RangeKey] = value + return qb +} + +// WithIndexRangeKeyLT sets range key condition for any index with LT operator. +// Only works with simple range keys, not composite ones. +// Example: query.WithIndexRangeKeyLT("timestamp-index", cutoffTime) +func (qb *QueryBuilder) WithIndexRangeKeyLT(indexName string, value any) *QueryBuilder { + index := qb.getIndexByName(indexName) + if index == nil || index.RangeKey == "" || index.RangeKeyParts != nil { + return qb + } + qb.KeyConditions[index.RangeKey] = expression.Key(index.RangeKey).LessThan(expression.Value(value)) + qb.UsedKeys[index.RangeKey] = true + qb.Attributes[index.RangeKey] = value + return qb +} + +// WithIndexRangeKeyGTE sets range key condition for any index with GTE operator. +// Only works with simple range keys, not composite ones. +// Example: query.WithIndexRangeKeyGTE("score-index", 100) +func (qb *QueryBuilder) WithIndexRangeKeyGTE(indexName string, value any) *QueryBuilder { + index := qb.getIndexByName(indexName) + if index == nil || index.RangeKey == "" || index.RangeKeyParts != nil { + return qb + } + qb.KeyConditions[index.RangeKey] = expression.Key(index.RangeKey).GreaterThanEqual(expression.Value(value)) + qb.UsedKeys[index.RangeKey] = true + qb.Attributes[index.RangeKey] = value + return qb +} + +// WithIndexRangeKeyLTE sets range key condition for any index with LTE operator. +// Only works with simple range keys, not composite ones. +// Example: query.WithIndexRangeKeyLTE("timestamp-index", cutoffTime) +func (qb *QueryBuilder) WithIndexRangeKeyLTE(indexName string, value any) *QueryBuilder { + index := qb.getIndexByName(indexName) + if index == nil || index.RangeKey == "" || index.RangeKeyParts != nil { + return qb + } + qb.KeyConditions[index.RangeKey] = expression.Key(index.RangeKey).LessThanEqual(expression.Value(value)) + qb.UsedKeys[index.RangeKey] = true + qb.Attributes[index.RangeKey] = value + return qb +} +` diff --git a/templates/v2/query/universal.go b/templates/v2/query/universal.go deleted file mode 100644 index 6ec7892..0000000 --- a/templates/v2/query/universal.go +++ /dev/null @@ -1,165 +0,0 @@ -package query - -// QueryBuilderUniversalTemplate provides universal operator support for QueryBuilder -const QueryBuilderUniversalTemplate = ` -// With adds a key condition using the universal operator system. -// Only works with partition and sort key attributes - non-key attributes are ignored. -// Provides type-safe operator validation based on the schema field types. -// Example: query.With("user_id", EQ, "123").With("created_at", GT, timestamp) -func (qb *QueryBuilder) With(field string, op OperatorType, values ...any) *QueryBuilder { - if !ValidateValues(op, values) { - return qb - } - - fieldInfo, exists := TableSchema.FieldsMap[field] - if !exists { - return qb - } - if !fieldInfo.IsKey { - return qb - } - if !ValidateOperator(fieldInfo.DynamoType, op) { - return qb - } - - keyCond, err := BuildKeyConditionExpression(field, op, values) - if err != nil { - return qb - } - qb.KeyConditions[field] = keyCond - qb.UsedKeys[field] = true - - if op == EQ && len(values) == 1 { - qb.Attributes[field] = values[0] - } - return qb -} - -// Filter adds a filter condition using the universal operator system. -// Works with any table attribute and validates operator compatibility with field types. -// Uses O(1) schema lookup for efficient field validation and type checking. -// Example: query.Filter("status", EQ, "active").Filter("tags", CONTAINS, "premium") -func (qb *QueryBuilder) Filter(field string, op OperatorType, values ...any) *QueryBuilder { - if !ValidateValues(op, values) { - return qb - } - - fieldInfo, exists := TableSchema.FieldsMap[field] - if !exists { - return qb - } - if !ValidateOperator(fieldInfo.DynamoType, op) { - return qb - } - - filterCond, err := BuildConditionExpression(field, op, values) - if err != nil { - return qb - } - qb.FilterConditions = append(qb.FilterConditions, filterCond) - qb.UsedKeys[field] = true - - if op == EQ && len(values) == 1 { - qb.Attributes[field] = values[0] - } - return qb -} - -// WithEQ is a convenience method for equality key conditions. -// Required for partition keys, commonly used for sort keys. -// Example: query.WithEQ("user_id", "123") -func (qb *QueryBuilder) WithEQ(field string, value any) *QueryBuilder { - return qb.With(field, EQ, value) -} - -// WithBetween is a convenience method for range key conditions. -// Only valid for sort keys, not partition keys. -// Example: query.WithBetween("timestamp", startTime, endTime) -func (qb *QueryBuilder) WithBetween(field string, start, end any) *QueryBuilder { - return qb.With(field, BETWEEN, start, end) -} - -// WithGT is a convenience method for greater than key conditions. -// Only valid for sort keys in range queries. -// Example: query.WithGT("created_at", yesterday) -func (qb *QueryBuilder) WithGT(field string, value any) *QueryBuilder { - return qb.With(field, GT, value) -} - -// WithGTE is a convenience method for greater than or equal key conditions. -// Only valid for sort keys in range queries. -// Example: query.WithGTE("score", minimumScore) -func (qb *QueryBuilder) WithGTE(field string, value any) *QueryBuilder { - return qb.With(field, GTE, value) -} - -// WithLT is a convenience method for less than key conditions. -// Only valid for sort keys in range queries. -// Example: query.WithLT("expiry_date", now) -func (qb *QueryBuilder) WithLT(field string, value any) *QueryBuilder { - return qb.With(field, LT, value) -} - -// WithLTE is a convenience method for less than or equal key conditions. -// Only valid for sort keys in range queries. -// Example: query.WithLTE("price", maxBudget) -func (qb *QueryBuilder) WithLTE(field string, value any) *QueryBuilder { - return qb.With(field, LTE, value) -} - -// FilterEQ is a convenience method for equality filter conditions. -// Works with any non-key attribute for post-query filtering. -// Example: query.FilterEQ("status", "active") -func (qb *QueryBuilder) FilterEQ(field string, value any) *QueryBuilder { - return qb.Filter(field, EQ, value) -} - -// FilterContains is a convenience method for contains filter conditions. -// Works with String attributes (substring) and Set attributes (membership). -// Example: query.FilterContains("description", "urgent") or query.FilterContains("tags", "vip") -func (qb *QueryBuilder) FilterContains(field string, value any) *QueryBuilder { - return qb.Filter(field, CONTAINS, value) -} - -// FilterBeginsWith is a convenience method for begins_with filter conditions. -// Only works with String attributes for prefix matching. -// Example: query.FilterBeginsWith("email", "admin@") -func (qb *QueryBuilder) FilterBeginsWith(field string, value any) *QueryBuilder { - return qb.Filter(field, BEGINS_WITH, value) -} - -// FilterBetween is a convenience method for range filter conditions. -// Works with comparable types (strings, numbers, dates). -// Example: query.FilterBetween("score", 80, 100) -func (qb *QueryBuilder) FilterBetween(field string, start, end any) *QueryBuilder { - return qb.Filter(field, BETWEEN, start, end) -} - -// FilterGT is a convenience method for greater than filter conditions. -// Works with comparable types for post-query filtering. -// Example: query.FilterGT("last_login", cutoffDate) -func (qb *QueryBuilder) FilterGT(field string, value any) *QueryBuilder { - return qb.Filter(field, GT, value) -} - -// FilterGTE is a convenience method for greater than or equal filter conditions. -// Works with comparable types for inclusive range filtering. -// Example: query.FilterGTE("age", minimumAge) -func (qb *QueryBuilder) FilterGTE(field string, value any) *QueryBuilder { - return qb.Filter(field, GTE, value) -} - -// FilterLT is a convenience method for less than filter conditions. -// Works with comparable types for upper bound filtering. -// Example: query.FilterLT("attempts", maxAttempts) -func (qb *QueryBuilder) FilterLT(field string, value any) *QueryBuilder { - return qb.Filter(field, LT, value) -} - -// FilterLTE is a convenience method for less than or equal filter conditions. -// Works with comparable types for inclusive upper bound filtering. -// Example: query.FilterLTE("file_size", maxFileSize) -func (qb *QueryBuilder) FilterLTE(field string, value any) *QueryBuilder { - return qb.Filter(field, LTE, value) -} -` diff --git a/templates/v2/scan/builder.go b/templates/v2/scan/builder.go index 30a9253..f707a15 100644 --- a/templates/v2/scan/builder.go +++ b/templates/v2/scan/builder.go @@ -32,117 +32,6 @@ func NewScanBuilder() *ScanBuilder { } } -// Filter adds a filter condition and returns ScanBuilder for method chaining. -// Filters are applied after items are read from DynamoDB. -// Example: scan.Filter("score", GT, 80) -func (sb *ScanBuilder) Filter(field string, op OperatorType, values ...any) *ScanBuilder { - sb.FilterMixin.Filter(field, op, values...) - return sb -} - -// FilterEQ adds equality filter and returns ScanBuilder for method chaining. -// Example: scan.FilterEQ("status", "active") -func (sb *ScanBuilder) FilterEQ(field string, value any) *ScanBuilder { - sb.FilterMixin.FilterEQ(field, value) - return sb -} - -// FilterContains adds contains filter and returns ScanBuilder for method chaining. -// Works with String attributes (substring) and Set attributes (membership). -// Example: scan.FilterContains("tags", "premium") -func (sb *ScanBuilder) FilterContains(field string, value any) *ScanBuilder { - sb.FilterMixin.FilterContains(field, value) - return sb -} - -// FilterNotContains adds not contains filter and returns ScanBuilder for method chaining. -// Opposite of FilterContains for exclusion filtering. -func (sb *ScanBuilder) FilterNotContains(field string, value any) *ScanBuilder { - sb.FilterMixin.FilterNotContains(field, value) - return sb -} - -// FilterBeginsWith adds begins_with filter and returns ScanBuilder for method chaining. -// Only works with String attributes for prefix matching. -// Example: scan.FilterBeginsWith("email", "admin@") -func (sb *ScanBuilder) FilterBeginsWith(field string, value any) *ScanBuilder { - sb.FilterMixin.FilterBeginsWith(field, value) - return sb -} - -// FilterBetween adds range filter and returns ScanBuilder for method chaining. -// Works with comparable types for inclusive range filtering. -// Example: scan.FilterBetween("score", 80, 100) -func (sb *ScanBuilder) FilterBetween(field string, start, end any) *ScanBuilder { - sb.FilterMixin.FilterBetween(field, start, end) - return sb -} - -// FilterGT adds greater than filter and returns ScanBuilder for method chaining. -// Example: scan.FilterGT("last_login", cutoffDate) -func (sb *ScanBuilder) FilterGT(field string, value any) *ScanBuilder { - sb.FilterMixin.FilterGT(field, value) - return sb -} - -// FilterLT adds less than filter and returns ScanBuilder for method chaining. -// Example: scan.FilterLT("attempts", maxAttempts) -func (sb *ScanBuilder) FilterLT(field string, value any) *ScanBuilder { - sb.FilterMixin.FilterLT(field, value) - return sb -} - -// FilterGTE adds greater than or equal filter and returns ScanBuilder for method chaining. -// Example: scan.FilterGTE("age", minimumAge) -func (sb *ScanBuilder) FilterGTE(field string, value any) *ScanBuilder { - sb.FilterMixin.FilterGTE(field, value) - return sb -} - -// FilterLTE adds less than or equal filter and returns ScanBuilder for method chaining. -// Example: scan.FilterLTE("file_size", maxFileSize) -func (sb *ScanBuilder) FilterLTE(field string, value any) *ScanBuilder { - sb.FilterMixin.FilterLTE(field, value) - return sb -} - -// FilterExists adds attribute exists filter and returns ScanBuilder for method chaining. -// Checks if the specified attribute exists in the item. -// Example: scan.FilterExists("optional_field") -func (sb *ScanBuilder) FilterExists(field string) *ScanBuilder { - sb.FilterMixin.FilterExists(field) - return sb -} - -// FilterNotExists adds attribute not exists filter and returns ScanBuilder for method chaining. -// Checks if the specified attribute does not exist in the item. -func (sb *ScanBuilder) FilterNotExists(field string) *ScanBuilder { - sb.FilterMixin.FilterNotExists(field) - return sb -} - -// FilterNE adds not equal filter and returns ScanBuilder for method chaining. -// Example: scan.FilterNE("status", "deleted") -func (sb *ScanBuilder) FilterNE(field string, value any) *ScanBuilder { - sb.FilterMixin.FilterNE(field, value) - return sb -} - -// FilterIn adds IN filter and returns ScanBuilder for method chaining. -// For scalar values only - use FilterContains for DynamoDB Sets. -// Example: scan.FilterIn("category", "books", "electronics", "clothing") -func (sb *ScanBuilder) FilterIn(field string, values ...any) *ScanBuilder { - sb.FilterMixin.FilterIn(field, values...) - return sb -} - -// FilterNotIn adds NOT_IN filter and returns ScanBuilder for method chaining. -// For scalar values only - use FilterNotContains for DynamoDB Sets. -func (sb *ScanBuilder) FilterNotIn(field string, values ...any) *ScanBuilder { - sb.FilterMixin.FilterNotIn(field, values...) - return sb -} - // Limit sets the maximum number of items and returns ScanBuilder for method chaining. // Controls the number of items returned in a single scan request. // Note: DynamoDB may return fewer items due to size limits even with this setting. diff --git a/templates/v2/scan/builder_filter.go b/templates/v2/scan/builder_filter.go new file mode 100644 index 0000000..53a2744 --- /dev/null +++ b/templates/v2/scan/builder_filter.go @@ -0,0 +1,119 @@ +package scan + +// ScanBuilderFilterTemplate provides Filter methods for scan conditions +const ScanBuilderFilterTemplate = ` +// Filter adds a filter condition and returns ScanBuilder for method chaining. +// Wraps FilterMixin.Filter with fluent interface support. +func (sb *ScanBuilder) Filter(field string, op OperatorType, values ...any) *ScanBuilder { + sb.FilterMixin.Filter(field, op, values...) + return sb +} +` + +// ScanBuilderFilterSugarTemplate provides convenience Filter methods (only for ALL mode) +const ScanBuilderFilterSugarTemplate = ` +// CONVENIENCE METHODS - Only available in ALL mode + +// FilterEQ adds equality filter and returns ScanBuilder for method chaining. +// Example: scan.FilterEQ("status", "active") +func (sb *ScanBuilder) FilterEQ(field string, value any) *ScanBuilder { + sb.FilterMixin.FilterEQ(field, value) + return sb +} + +// FilterContains adds contains filter and returns ScanBuilder for method chaining. +// Works with String attributes (substring) and Set attributes (membership). +// Example: scan.FilterContains("tags", "premium") +func (sb *ScanBuilder) FilterContains(field string, value any) *ScanBuilder { + sb.FilterMixin.FilterContains(field, value) + return sb +} + +// FilterNotContains adds not contains filter and returns ScanBuilder for method chaining. +// Opposite of FilterContains for exclusion filtering. +func (sb *ScanBuilder) FilterNotContains(field string, value any) *ScanBuilder { + sb.FilterMixin.FilterNotContains(field, value) + return sb +} + +// FilterBeginsWith adds begins_with filter and returns ScanBuilder for method chaining. +// Only works with String attributes for prefix matching. +// Example: scan.FilterBeginsWith("email", "admin@") +func (sb *ScanBuilder) FilterBeginsWith(field string, value any) *ScanBuilder { + sb.FilterMixin.FilterBeginsWith(field, value) + return sb +} + +// FilterBetween adds range filter and returns ScanBuilder for method chaining. +// Works with comparable types for inclusive range filtering. +// Example: scan.FilterBetween("score", 80, 100) +func (sb *ScanBuilder) FilterBetween(field string, start, end any) *ScanBuilder { + sb.FilterMixin.FilterBetween(field, start, end) + return sb +} + +// FilterGT adds greater than filter and returns ScanBuilder for method chaining. +// Example: scan.FilterGT("last_login", cutoffDate) +func (sb *ScanBuilder) FilterGT(field string, value any) *ScanBuilder { + sb.FilterMixin.FilterGT(field, value) + return sb +} + +// FilterLT adds less than filter and returns ScanBuilder for method chaining. +// Example: scan.FilterLT("attempts", maxAttempts) +func (sb *ScanBuilder) FilterLT(field string, value any) *ScanBuilder { + sb.FilterMixin.FilterLT(field, value) + return sb +} + +// FilterGTE adds greater than or equal filter and returns ScanBuilder for method chaining. +// Example: scan.FilterGTE("age", minimumAge) +func (sb *ScanBuilder) FilterGTE(field string, value any) *ScanBuilder { + sb.FilterMixin.FilterGTE(field, value) + return sb +} + +// FilterLTE adds less than or equal filter and returns ScanBuilder for method chaining. +// Example: scan.FilterLTE("file_size", maxFileSize) +func (sb *ScanBuilder) FilterLTE(field string, value any) *ScanBuilder { + sb.FilterMixin.FilterLTE(field, value) + return sb +} + +// FilterExists adds attribute exists filter and returns ScanBuilder for method chaining. +// Checks if the specified attribute exists in the item. +// Example: scan.FilterExists("optional_field") +func (sb *ScanBuilder) FilterExists(field string) *ScanBuilder { + sb.FilterMixin.FilterExists(field) + return sb +} + +// FilterNotExists adds attribute not exists filter and returns ScanBuilder for method chaining. +// Checks if the specified attribute does not exist in the item. +func (sb *ScanBuilder) FilterNotExists(field string) *ScanBuilder { + sb.FilterMixin.FilterNotExists(field) + return sb +} + +// FilterNE adds not equal filter and returns ScanBuilder for method chaining. +// Example: scan.FilterNE("status", "deleted") +func (sb *ScanBuilder) FilterNE(field string, value any) *ScanBuilder { + sb.FilterMixin.FilterNE(field, value) + return sb +} + +// FilterIn adds IN filter and returns ScanBuilder for method chaining. +// For scalar values only - use FilterContains for DynamoDB Sets. +// Example: scan.FilterIn("category", "books", "electronics", "clothing") +func (sb *ScanBuilder) FilterIn(field string, values ...any) *ScanBuilder { + sb.FilterMixin.FilterIn(field, values...) + return sb +} + +// FilterNotIn adds NOT_IN filter and returns ScanBuilder for method chaining. +// For scalar values only - use FilterNotContains for DynamoDB Sets. +func (sb *ScanBuilder) FilterNotIn(field string, values ...any) *ScanBuilder { + sb.FilterMixin.FilterNotIn(field, values...) + return sb +} +` diff --git a/templates/v2/template.go b/templates/v2/template.go index ae1d8cf..4ef8e98 100644 --- a/templates/v2/template.go +++ b/templates/v2/template.go @@ -22,12 +22,27 @@ package {{.PackageName}} ` + core.SchemaTemplate + ` ` + core.MixinsTemplate + ` - -` + query.QueryBuilderTemplate + query.QueryBuilderBuildTemplate + query.QueryBuilderUtilsTemplate + ` - -` + scan.ScanBuilderTemplate + scan.ScanBuilderBuildTemplate + ` +{{if IsALL .Mode}} +` + core.FilterMixinSugarTemplate + core.KeyConditionMixinSugarTemplate + ` +{{end}} + +` + query.QueryBuilderTemplate + query.QueryBuilderWithTemplate + query.QueryBuilderFilterTemplate + ` +{{if IsALL .Mode}} +` + query.QueryBuilderWithSugarTemplate + query.QueryBuilderFilterSugarTemplate + ` +{{end}} +` + query.QueryBuilderBuildTemplate + query.QueryBuilderUtilsTemplate + ` + +` + scan.ScanBuilderTemplate + scan.ScanBuilderFilterTemplate + ` +{{if IsALL .Mode}} +` + scan.ScanBuilderFilterSugarTemplate + ` +{{end}} +` + scan.ScanBuilderBuildTemplate + ` ` + inputs.ItemInputsTemplate + inputs.UpdateInputsTemplate + inputs.DeleteInputsTemplate + inputs.KeyInputsTemplate + ` -` + helpers.AtomicHelpersTemplate + helpers.StreamHelpersTemplate + helpers.ConverterHelpersTemplate + helpers.MarshalingHelpersTemplate + helpers.ValidationHelpersTemplate + ` +` + helpers.AtomicHelpersTemplate + ` +{{if IsALL .Mode}} +` + helpers.StreamHelpersTemplate + ` +{{end}} +` + helpers.ConverterHelpersTemplate + helpers.MarshalingHelpersTemplate + helpers.ValidationHelpersTemplate + ` ` diff --git a/templates/v2/v2.go b/templates/v2/v2.go index 69c260a..9e96d6e 100644 --- a/templates/v2/v2.go +++ b/templates/v2/v2.go @@ -9,6 +9,7 @@ package v2 import ( "github.com/Mad-Pixels/go-dyno/internal/generator/attribute" "github.com/Mad-Pixels/go-dyno/internal/generator/index" + "github.com/Mad-Pixels/go-dyno/internal/generator/mode" ) // TemplateMap defines the full set of metadata used to generate DynamoDB-related code. @@ -26,6 +27,9 @@ type TemplateMap struct { // RangeKey is the optional sort key of the table. RangeKey string + // Mode determines what code to generate (ALL, MIN, etc). + Mode mode.Mode + // Attributes are the table-specific attributes defined in the schema. Attributes []attribute.Attribute diff --git a/tests/fixtures/base-boolean.json b/tests/fixtures/base-boolean__all.json similarity index 89% rename from tests/fixtures/base-boolean.json rename to tests/fixtures/base-boolean__all.json index 841f554..a536227 100644 --- a/tests/fixtures/base-boolean.json +++ b/tests/fixtures/base-boolean__all.json @@ -1,5 +1,5 @@ { - "table_name": "base-boolean", + "table_name": "base-boolean-all", "hash_key": "id", "range_key": "version", "attributes": [ diff --git a/tests/fixtures/base-boolean__min.json b/tests/fixtures/base-boolean__min.json new file mode 100644 index 0000000..691e507 --- /dev/null +++ b/tests/fixtures/base-boolean__min.json @@ -0,0 +1,14 @@ +{ + "table_name": "base-boolean-min", + "hash_key": "id", + "range_key": "version", + "attributes": [ + { "name": "id", "type": "S" }, + { "name": "version", "type": "N" } + ], + "common_attributes": [ + { "name": "is_active", "type": "BOOL" }, + { "name": "is_published", "type": "BOOL" } + ], + "secondary_indexes": [] +} \ No newline at end of file diff --git a/tests/fixtures/base-mixed.json b/tests/fixtures/base-mixed.json deleted file mode 100644 index 91ac9cd..0000000 --- a/tests/fixtures/base-mixed.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "table_name": "base-mixed", - "hash_key": "pk", - "range_key": "sk", - "attributes": [ - { "name": "pk", "type": "S" }, - { "name": "sk", "type": "S" } - ], - "common_attributes": [ - { "name": "name", "type": "S" }, - { "name": "count", "type": "N" }, - { "name": "is_active", "type": "BOOL" }, - { "name": "tags", "type": "SS" }, - { "name": "scores", "type": "NS" } - ], - "secondary_indexes": [] -} \ No newline at end of file diff --git a/tests/fixtures/base-number.json b/tests/fixtures/base-number__all.json similarity index 88% rename from tests/fixtures/base-number.json rename to tests/fixtures/base-number__all.json index bb02e59..a5c7be0 100644 --- a/tests/fixtures/base-number.json +++ b/tests/fixtures/base-number__all.json @@ -1,5 +1,5 @@ { - "table_name": "base-number", + "table_name": "base-number-all", "hash_key": "id", "range_key": "timestamp", "attributes": [ diff --git a/tests/fixtures/base-number__min.json b/tests/fixtures/base-number__min.json new file mode 100644 index 0000000..2b30b49 --- /dev/null +++ b/tests/fixtures/base-number__min.json @@ -0,0 +1,14 @@ +{ + "table_name": "base-number-min", + "hash_key": "id", + "range_key": "timestamp", + "attributes": [ + { "name": "id", "type": "S" }, + { "name": "timestamp", "type": "N" } + ], + "common_attributes": [ + { "name": "count", "type": "N" }, + { "name": "price", "type": "N" } + ], + "secondary_indexes": [] +} \ No newline at end of file diff --git a/tests/fixtures/base-set-number.json b/tests/fixtures/base-set-number__all.json similarity index 88% rename from tests/fixtures/base-set-number.json rename to tests/fixtures/base-set-number__all.json index 244d174..81e64ad 100644 --- a/tests/fixtures/base-set-number.json +++ b/tests/fixtures/base-set-number__all.json @@ -1,5 +1,5 @@ { - "table_name": "base-set-number", + "table_name": "base-set-number-all", "hash_key": "user_id", "range_key": "session_id", "attributes": [ diff --git a/tests/fixtures/base-set-string.json b/tests/fixtures/base-set-string__all.json similarity index 87% rename from tests/fixtures/base-set-string.json rename to tests/fixtures/base-set-string__all.json index e072eac..5aa957c 100644 --- a/tests/fixtures/base-set-string.json +++ b/tests/fixtures/base-set-string__all.json @@ -1,5 +1,5 @@ { - "table_name": "base-set-string", + "table_name": "base-set-string-all", "hash_key": "id", "range_key": "group_id", "attributes": [ diff --git a/tests/fixtures/base-string.json b/tests/fixtures/base-string__all.json similarity index 89% rename from tests/fixtures/base-string.json rename to tests/fixtures/base-string__all.json index 963b1e7..3dfbcb4 100644 --- a/tests/fixtures/base-string.json +++ b/tests/fixtures/base-string__all.json @@ -1,5 +1,5 @@ { - "table_name": "base-string", + "table_name": "base-string-all", "hash_key": "id", "range_key": "category", "attributes": [ diff --git a/tests/fixtures/base-string__min.json b/tests/fixtures/base-string__min.json new file mode 100644 index 0000000..5317858 --- /dev/null +++ b/tests/fixtures/base-string__min.json @@ -0,0 +1,14 @@ +{ + "table_name": "base-string-min", + "hash_key": "id", + "range_key": "category", + "attributes": [ + { "name": "id", "type": "S" }, + { "name": "category", "type": "S" } + ], + "common_attributes": [ + { "name": "title", "type": "S" }, + { "name": "description", "type": "S" } + ], + "secondary_indexes": [] +} \ No newline at end of file diff --git a/tests/fixtures/custom-number.json b/tests/fixtures/custom-number__all.json similarity index 94% rename from tests/fixtures/custom-number.json rename to tests/fixtures/custom-number__all.json index d869d00..b6caa5e 100644 --- a/tests/fixtures/custom-number.json +++ b/tests/fixtures/custom-number__all.json @@ -1,5 +1,5 @@ { - "table_name": "custom-number", + "table_name": "custom-number-all", "hash_key": "id", "range_key": "timestamp", "attributes": [ diff --git a/tests/fixtures/custom-set-number.json b/tests/fixtures/custom-set-number__all.json similarity index 94% rename from tests/fixtures/custom-set-number.json rename to tests/fixtures/custom-set-number__all.json index 3fc824a..0488a2e 100644 --- a/tests/fixtures/custom-set-number.json +++ b/tests/fixtures/custom-set-number__all.json @@ -1,5 +1,5 @@ { - "table_name": "custom-set-number", + "table_name": "custom-set-number-all", "hash_key": "id", "range_key": "group_id", "attributes": [ diff --git a/tests/fixtures/user-posts-complete.json b/tests/fixtures/user-posts-complete__all.json similarity index 97% rename from tests/fixtures/user-posts-complete.json rename to tests/fixtures/user-posts-complete__all.json index d73f1d4..847ad4f 100644 --- a/tests/fixtures/user-posts-complete.json +++ b/tests/fixtures/user-posts-complete__all.json @@ -1,5 +1,5 @@ { - "table_name": "user-posts-complete", + "table_name": "user-posts-complete-all", "hash_key": "user_id", "range_key": "created_at", "attributes": [ diff --git a/tests/fixtures/user-posts-complete__min.json b/tests/fixtures/user-posts-complete__min.json new file mode 100644 index 0000000..8ae45e2 --- /dev/null +++ b/tests/fixtures/user-posts-complete__min.json @@ -0,0 +1,62 @@ +{ + "table_name": "user-posts-complete-min", + "hash_key": "user_id", + "range_key": "created_at", + "attributes": [ + { "name": "user_id", "type": "S" }, + { "name": "created_at", "type": "S" }, + { "name": "post_type", "type": "S" }, + { "name": "status", "type": "S" }, + { "name": "priority", "type": "N" }, + { "name": "category", "type": "S" }, + { "name": "title", "type": "S" } + ], + "common_attributes": [ + { "name": "content", "type": "S" }, + { "name": "tags", "type": "SS" }, + { "name": "view_count", "type": "N" }, + { "name": "updated_at", "type": "S" } + ], + "secondary_indexes": [ + { + "name": "lsi_by_post_type", + "type": "LSI", + "range_key": "post_type", + "projection_type": "ALL" + }, + { + "name": "lsi_by_status", + "type": "LSI", + "range_key": "status", + "projection_type": "KEYS_ONLY" + }, + { + "name": "lsi_by_priority", + "type": "LSI", + "range_key": "priority", + "projection_type": "INCLUDE", + "non_key_attributes": ["title", "content"] + }, + { + "name": "gsi_by_category", + "type": "GSI", + "hash_key": "category", + "range_key": "created_at", + "projection_type": "ALL" + }, + { + "name": "gsi_by_title", + "type": "GSI", + "hash_key": "title", + "projection_type": "KEYS_ONLY" + }, + { + "name": "gsi_by_status_priority", + "type": "GSI", + "hash_key": "status", + "range_key": "priority", + "projection_type": "INCLUDE", + "non_key_attributes": ["user_id", "title", "view_count"] + } + ] +} \ No newline at end of file diff --git a/tests/localstack/base_boolean_test.go b/tests/localstack/base_boolean_all_test.go similarity index 98% rename from tests/localstack/base_boolean_test.go rename to tests/localstack/base_boolean_all_test.go index 5d2c62b..2d2a340 100644 --- a/tests/localstack/base_boolean_test.go +++ b/tests/localstack/base_boolean_all_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - baseboolean "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/baseboolean" + baseboolean "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basebooleanall" ) // TestBaseBoolean focuses on Boolean (BOOL) type operations and functionality. @@ -24,8 +24,8 @@ import ( // - Boolean state transitions and filtering // - Edge cases (true/false consistency) // -// Schema: base-boolean.json -// - Table: "base-boolean" +// Schema: base-boolean__all.json +// - Table: "base-boolean-all" // - Hash Key: id (S) // - Range Key: version (N) // - Common: is_active (BOOL), is_published (BOOL) @@ -366,7 +366,6 @@ func testBooleanInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Cont // ==================== Boolean QueryBuilder Tests ==================== func testBooleanQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.Context) { - // Setup test data setupBooleanTestData(t, client, ctx) t.Run("boolean_hash_key_query", func(t *testing.T) { @@ -526,7 +525,6 @@ func testBooleanScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.C func testBooleanStateTransitions(t *testing.T, client *dynamodb.Client, ctx context.Context) { t.Run("activation_workflow", func(t *testing.T) { - // Create inactive, unpublished item item := baseboolean.SchemaItem{ Id: "workflow-test", Version: 1, @@ -622,7 +620,7 @@ func testBooleanSchema(t *testing.T) { t.Run("boolean_table_schema", func(t *testing.T) { schema := baseboolean.TableSchema - assert.Equal(t, "base-boolean", schema.TableName, "Table name should match") + assert.Equal(t, "base-boolean-all", schema.TableName, "Table name should match") assert.Equal(t, "id", schema.HashKey, "Hash key should be 'id'") assert.Equal(t, "version", schema.RangeKey, "Range key should be 'version'") assert.Len(t, schema.SecondaryIndexes, 0, "Should have no secondary indexes") @@ -631,10 +629,9 @@ func testBooleanSchema(t *testing.T) { }) t.Run("boolean_attributes", func(t *testing.T) { - // Check primary attributes expectedPrimary := map[string]string{ - "id": "S", // hash key is string - "version": "N", // range key is number + "id": "S", + "version": "N", } for _, attr := range baseboolean.TableSchema.Attributes { @@ -643,7 +640,6 @@ func testBooleanSchema(t *testing.T) { assert.Equal(t, expectedType, attr.Type, "Attribute %s should have correct type", attr.Name) } - // Check common attributes (all boolean type) expectedCommon := map[string]string{ "is_active": "BOOL", "is_published": "BOOL", @@ -659,7 +655,7 @@ func testBooleanSchema(t *testing.T) { }) t.Run("boolean_constants", func(t *testing.T) { - assert.Equal(t, "base-boolean", baseboolean.TableName, "TableName constant should be correct") + assert.Equal(t, "base-boolean-all", baseboolean.TableName, "TableName constant should be correct") assert.Equal(t, "id", baseboolean.ColumnId, "ColumnId should be correct") assert.Equal(t, "version", baseboolean.ColumnVersion, "ColumnVersion should be correct") assert.Equal(t, "is_active", baseboolean.ColumnIsActive, "ColumnIsActive should be correct") diff --git a/tests/localstack/base_boolean_min_test.go b/tests/localstack/base_boolean_min_test.go new file mode 100644 index 0000000..cc71884 --- /dev/null +++ b/tests/localstack/base_boolean_min_test.go @@ -0,0 +1,285 @@ +package localstack + +import ( + "context" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + basebooleanmin "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basebooleanmin" +) + +// TestBaseBooleanMIN focuses on Boolean (BOOL) type operations with minimal generated code. +// This test validates boolean functionality using only basic methods (no sugar methods). +// +// Test Coverage: +// - Basic Boolean CRUD operations using universal methods +// - Boolean marshaling/unmarshaling +// - Core Query and Scan operations using .With() and .Filter() +// - Schema validation +// +// Schema: base-boolean__min.json +// - Table: "base-boolean-min" +// - Hash Key: id (S) +// - Range Key: version (N) +// - Common: is_active (BOOL), is_published (BOOL) +// +// Note: MIN mode only includes universal methods like .With() and .Filter() +// No convenience methods like .WithEQ(), .FilterEQ() etc. +func TestBaseBooleanMIN(t *testing.T) { + client := ConnectToLocalStack(t, DefaultLocalStackConfig()) + ctx, cancel := TestContext(3 * time.Minute) + defer cancel() + + t.Logf("Testing Boolean MIN operations on: %s", basebooleanmin.TableName) + + t.Run("Boolean_MIN_BasicCRUD", func(t *testing.T) { + testBooleanMINBasicCRUD(t, client, ctx) + }) + + t.Run("Boolean_MIN_QueryBuilder", func(t *testing.T) { + testBooleanMINQueryBuilder(t, client, ctx) + }) + + t.Run("Boolean_MIN_ScanBuilder", func(t *testing.T) { + testBooleanMINScanBuilder(t, client, ctx) + }) + + t.Run("Boolean_MIN_Schema", func(t *testing.T) { + t.Parallel() + testBooleanMINSchema(t) + }) +} + +// ==================== Boolean MIN Basic CRUD ==================== + +func testBooleanMINBasicCRUD(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Run("min_create_and_read", func(t *testing.T) { + item := basebooleanmin.SchemaItem{ + Id: "min-test-001", + Version: 1, + IsActive: true, + IsPublished: false, + } + + av, err := basebooleanmin.ItemInput(item) + require.NoError(t, err, "Should marshal boolean item in MIN mode") + assert.NotEmpty(t, av, "Marshaled item should not be empty") + + assert.IsType(t, &types.AttributeValueMemberS{}, av[basebooleanmin.ColumnId]) + assert.IsType(t, &types.AttributeValueMemberN{}, av[basebooleanmin.ColumnVersion]) + assert.IsType(t, &types.AttributeValueMemberBOOL{}, av[basebooleanmin.ColumnIsActive]) + assert.IsType(t, &types.AttributeValueMemberBOOL{}, av[basebooleanmin.ColumnIsPublished]) + + _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(basebooleanmin.TableName), + Item: av, + }) + require.NoError(t, err, "Should store boolean item in DynamoDB") + + key, err := basebooleanmin.KeyInput(item) + require.NoError(t, err, "Should create key from item") + + getResult, err := client.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(basebooleanmin.TableName), + Key: key, + }) + require.NoError(t, err, "Should retrieve boolean item") + assert.NotEmpty(t, getResult.Item, "Retrieved item should not be empty") + + assert.Equal(t, true, getResult.Item[basebooleanmin.ColumnIsActive].(*types.AttributeValueMemberBOOL).Value) + assert.Equal(t, false, getResult.Item[basebooleanmin.ColumnIsPublished].(*types.AttributeValueMemberBOOL).Value) + + t.Logf("✅ MIN mode basic CRUD operations work correctly") + }) + + t.Run("min_raw_operations", func(t *testing.T) { + key, err := basebooleanmin.KeyInputFromRaw("min-raw-001", 5) + require.NoError(t, err, "Should create key from raw values") + assert.NotEmpty(t, key, "Raw key should not be empty") + + updates := map[string]any{ + "is_active": false, + "is_published": true, + } + + updateInput, err := basebooleanmin.UpdateItemInputFromRaw("min-raw-001", 5, updates) + require.NoError(t, err, "Should create update input from raw values") + assert.NotNil(t, updateInput, "Update input should be created") + + t.Logf("✅ MIN mode raw operations work correctly") + }) +} + +// ==================== Boolean MIN QueryBuilder Tests ==================== + +func testBooleanMINQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.Context) { + setupBooleanMINTestData(t, client, ctx) + + t.Run("min_query_universal_methods", func(t *testing.T) { + qb := basebooleanmin.NewQueryBuilder(). + With("id", basebooleanmin.EQ, "min-query-test") + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build query using universal .With() method") + assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") + + t.Logf("✅ MIN mode universal .With() method works") + }) + + t.Run("min_query_range_conditions", func(t *testing.T) { + qb := basebooleanmin.NewQueryBuilder(). + With("id", basebooleanmin.EQ, "min-query-test"). + With("version", basebooleanmin.BETWEEN, 1, 3) + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build range query using universal operators") + assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") + + t.Logf("✅ MIN mode range conditions work with universal operators") + }) + + t.Run("min_query_with_filters", func(t *testing.T) { + qb := basebooleanmin.NewQueryBuilder(). + With("id", basebooleanmin.EQ, "min-query-test"). + Filter("is_active", basebooleanmin.EQ, true) + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build query with universal .Filter() method") + assert.NotNil(t, queryInput.FilterExpression, "Should have filter expression") + + t.Logf("✅ MIN mode universal .Filter() method works") + }) + + t.Run("min_query_execution", func(t *testing.T) { + qb := basebooleanmin.NewQueryBuilder(). + With("id", basebooleanmin.EQ, "min-query-test") + + items, err := qb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN mode query") + assert.NotEmpty(t, items, "Should return items") + + for _, item := range items { + assert.Equal(t, "min-query-test", item.Id, "All items should have correct hash key") + } + + t.Logf("✅ MIN mode query execution returned %d items", len(items)) + }) +} + +// ==================== Boolean MIN ScanBuilder Tests ==================== + +func testBooleanMINScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Run("min_scan_universal_filter", func(t *testing.T) { + sb := basebooleanmin.NewScanBuilder(). + Filter("is_active", basebooleanmin.EQ, true) + + scanInput, err := sb.BuildScan() + require.NoError(t, err, "Should build scan with universal .Filter() method") + assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") + + t.Logf("✅ MIN mode scan universal .Filter() method works") + }) + + t.Run("min_scan_multiple_filters", func(t *testing.T) { + sb := basebooleanmin.NewScanBuilder(). + Filter("is_active", basebooleanmin.EQ, true). + Filter("is_published", basebooleanmin.EQ, false). + Filter("version", basebooleanmin.GT, 0) + + scanInput, err := sb.BuildScan() + require.NoError(t, err, "Should build scan with multiple universal filters") + assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") + + t.Logf("✅ MIN mode multiple universal filters work") + }) + + t.Run("min_scan_execution", func(t *testing.T) { + sb := basebooleanmin.NewScanBuilder(). + Filter("id", basebooleanmin.EQ, "min-query-test"). + Limit(5) + + items, err := sb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN mode scan") + + for _, item := range items { + assert.Equal(t, "min-query-test", item.Id, "Items should match filter") + } + + t.Logf("✅ MIN mode scan execution returned %d items", len(items)) + }) +} + +// ==================== Boolean MIN Schema Tests ==================== + +func testBooleanMINSchema(t *testing.T) { + t.Run("min_schema_structure", func(t *testing.T) { + schema := basebooleanmin.TableSchema + + assert.Equal(t, "base-boolean-min", schema.TableName, "Table name should match MIN schema") + assert.Equal(t, "id", schema.HashKey, "Hash key should be 'id'") + assert.Equal(t, "version", schema.RangeKey, "Range key should be 'version'") + assert.Len(t, schema.SecondaryIndexes, 0, "Should have no secondary indexes") + + t.Logf("✅ MIN mode schema structure validated") + }) + + t.Run("min_constants", func(t *testing.T) { + assert.Equal(t, "base-boolean-min", basebooleanmin.TableName, "TableName constant should be correct") + assert.Equal(t, "id", basebooleanmin.ColumnId, "ColumnId should be correct") + assert.Equal(t, "version", basebooleanmin.ColumnVersion, "ColumnVersion should be correct") + assert.Equal(t, "is_active", basebooleanmin.ColumnIsActive, "ColumnIsActive should be correct") + assert.Equal(t, "is_published", basebooleanmin.ColumnIsPublished, "ColumnIsPublished should be correct") + + t.Logf("✅ MIN mode constants validated") + }) + + t.Run("min_operators_available", func(t *testing.T) { + assert.NotNil(t, basebooleanmin.EQ, "EQ operator should be available") + assert.NotNil(t, basebooleanmin.GT, "GT operator should be available") + assert.NotNil(t, basebooleanmin.LT, "LT operator should be available") + assert.NotNil(t, basebooleanmin.BETWEEN, "BETWEEN operator should be available") + + t.Logf("✅ MIN mode universal operators available") + }) + + t.Run("min_no_sugar_methods", func(t *testing.T) { + qb := basebooleanmin.NewQueryBuilder() + assert.NotNil(t, qb, "QueryBuilder should be available") + + sb := basebooleanmin.NewScanBuilder() + assert.NotNil(t, sb, "ScanBuilder should be available") + + t.Logf("✅ MIN mode builders available (sugar methods should be absent)") + }) +} + +// ==================== Helper Functions ==================== + +func setupBooleanMINTestData(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Helper() + + testItems := []basebooleanmin.SchemaItem{ + {Id: "min-query-test", Version: 1, IsActive: true, IsPublished: false}, + {Id: "min-query-test", Version: 2, IsActive: false, IsPublished: true}, + {Id: "min-query-test", Version: 3, IsActive: true, IsPublished: true}, + } + + for _, item := range testItems { + av, err := basebooleanmin.ItemInput(item) + require.NoError(t, err, "Should marshal MIN test item") + + _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(basebooleanmin.TableName), + Item: av, + }) + require.NoError(t, err, "Should store MIN test item") + } + + t.Logf("MIN setup complete: inserted %d boolean test items", len(testItems)) +} diff --git a/tests/localstack/base_number_test.go b/tests/localstack/base_number_all_test.go similarity index 98% rename from tests/localstack/base_number_test.go rename to tests/localstack/base_number_all_test.go index 036296c..3efa42a 100644 --- a/tests/localstack/base_number_test.go +++ b/tests/localstack/base_number_all_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - basenumber "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basenumber" + basenumber "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basenumberall" ) // TestBaseNumber focuses on Number (N) type operations and functionality. @@ -25,8 +25,8 @@ import ( // - Increment operations // - Edge cases (zero, negative, large numbers) // -// Schema: base-number.json -// - Table: "base-number" +// Schema: base-number__all.json +// - Table: "base-number-all" // - Hash Key: id (S) // - Range Key: timestamp (N) // - Common: count (N), price (N) @@ -77,7 +77,6 @@ func testNumberInput(t *testing.T, client *dynamodb.Client, ctx context.Context) Count: 42, Price: 1999, } - av, err := basenumber.ItemInput(item) require.NoError(t, err, "Should marshal number item") assert.NotEmpty(t, av, "Marshaled item should not be empty") @@ -97,7 +96,6 @@ func testNumberInput(t *testing.T, client *dynamodb.Client, ctx context.Context) Item: av, }) require.NoError(t, err, "Should store number item in DynamoDB") - t.Logf("✅ Created number item: %s/%d", item.Id, item.Timestamp) }) @@ -106,7 +104,6 @@ func testNumberInput(t *testing.T, client *dynamodb.Client, ctx context.Context) Id: "number-test-001", Timestamp: 1640995200, } - key, err := basenumber.KeyInput(item) require.NoError(t, err, "Should create key from item") @@ -121,7 +118,6 @@ func testNumberInput(t *testing.T, client *dynamodb.Client, ctx context.Context) assert.Equal(t, "1999", getResult.Item[basenumber.ColumnPrice].(*types.AttributeValueMemberN).Value) assert.Equal(t, "number-test-001", getResult.Item[basenumber.ColumnId].(*types.AttributeValueMemberS).Value) assert.Equal(t, "1640995200", getResult.Item[basenumber.ColumnTimestamp].(*types.AttributeValueMemberN).Value) - t.Logf("✅ Retrieved number item successfully") }) @@ -132,7 +128,6 @@ func testNumberInput(t *testing.T, client *dynamodb.Client, ctx context.Context) Count: 100, Price: 2499, } - updateInput, err := basenumber.UpdateItemInput(item) require.NoError(t, err, "Should create update input from item") @@ -145,10 +140,8 @@ func testNumberInput(t *testing.T, client *dynamodb.Client, ctx context.Context) Key: key, }) require.NoError(t, err, "Should retrieve updated item") - assert.Equal(t, "100", getResult.Item[basenumber.ColumnCount].(*types.AttributeValueMemberN).Value) assert.Equal(t, "2499", getResult.Item[basenumber.ColumnPrice].(*types.AttributeValueMemberN).Value) - t.Logf("✅ Updated number item successfully") }) @@ -157,7 +150,6 @@ func testNumberInput(t *testing.T, client *dynamodb.Client, ctx context.Context) Id: "number-test-001", Timestamp: 1640995200, } - deleteInput, err := basenumber.DeleteItemInput(item) require.NoError(t, err, "Should create delete input from item") @@ -171,7 +163,6 @@ func testNumberInput(t *testing.T, client *dynamodb.Client, ctx context.Context) }) require.NoError(t, err, "GetItem should not error for missing item") assert.Empty(t, getResult.Item, "Number item should be deleted") - t.Logf("✅ Deleted number item successfully") }) @@ -182,7 +173,6 @@ func testNumberInput(t *testing.T, client *dynamodb.Client, ctx context.Context) {Id: "edge-3", Timestamp: 9999999999, Count: 2147483647, Price: 999999999}, {Id: "edge-4", Timestamp: 1640995100, Count: 1, Price: 1}, } - for _, item := range edgeCases { av, err := basenumber.ItemInput(item) require.NoError(t, err, "Should handle number edge case: %s", item.Id) @@ -193,7 +183,6 @@ func testNumberInput(t *testing.T, client *dynamodb.Client, ctx context.Context) }) require.NoError(t, err, "Should store number edge case item: %s", item.Id) } - t.Logf("✅ Number edge cases handled successfully") }) } @@ -208,7 +197,6 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte Count: 75, Price: 3499, } - av, err := basenumber.ItemInput(item) require.NoError(t, err, "Should marshal number item") assert.NotEmpty(t, av, "Marshaled item should not be empty") @@ -218,7 +206,6 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte Item: av, }) require.NoError(t, err, "Should store number item in DynamoDB") - t.Logf("✅ Created number item for raw testing: %s/%d", item.Id, item.Timestamp) }) @@ -237,7 +224,6 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte assert.Equal(t, "3499", getResult.Item[basenumber.ColumnPrice].(*types.AttributeValueMemberN).Value) assert.Equal(t, "number-raw-001", getResult.Item[basenumber.ColumnId].(*types.AttributeValueMemberS).Value) assert.Equal(t, "1640995300", getResult.Item[basenumber.ColumnTimestamp].(*types.AttributeValueMemberN).Value) - t.Logf("✅ Retrieved number item successfully using raw key") }) @@ -259,10 +245,8 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte Key: key, }) require.NoError(t, err, "Should retrieve updated item") - assert.Equal(t, "150", getResult.Item[basenumber.ColumnCount].(*types.AttributeValueMemberN).Value) assert.Equal(t, "4999", getResult.Item[basenumber.ColumnPrice].(*types.AttributeValueMemberN).Value) - t.Logf("✅ Updated number item successfully using raw method") }) @@ -280,7 +264,6 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte }) require.NoError(t, err, "GetItem should not error for missing item") assert.Empty(t, getResult.Item, "Number item should be deleted") - t.Logf("✅ Deleted number item successfully using raw method") }) @@ -296,7 +279,6 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte require.NoError(t, err, "Should create key from object") assert.Equal(t, keyFromRaw, keyFromObject, "Raw and object-based keys should be identical") - t.Logf("✅ Raw and object-based number methods produce identical results") }) @@ -322,7 +304,6 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte updates: map[string]any{"count": 2147483647, "price": 999999999}, }, } - for _, edgeCase := range edgeCases { updateInput, err := basenumber.UpdateItemInputFromRaw(edgeCase.id, edgeCase.timestamp, edgeCase.updates) require.NoError(t, err, "Should handle raw number edge case: %s", edgeCase.id) @@ -332,7 +313,6 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte require.NoError(t, err, "Should create delete input for edge case: %s", edgeCase.id) assert.NotNil(t, deleteInput, "Delete input should be created") } - t.Logf("✅ Raw number edge cases handled successfully") }) @@ -345,7 +325,6 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte decrementInput, err := basenumber.IncrementAttribute("increment-raw-test", 1640995888, "price", -5) require.NoError(t, err, "Should create decrement input with raw method") assert.NotNil(t, decrementInput.UpdateExpression, "Should have update expression") - t.Logf("✅ Raw increment operations work correctly") }) @@ -355,7 +334,6 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte conditionValues := map[string]types.AttributeValue{ ":v": &types.AttributeValueMemberN{Value: "1"}, } - deleteInput, err := basenumber.DeleteItemInputWithCondition( "conditional-test", 1640995777, conditionExpr, conditionNames, conditionValues, @@ -369,14 +347,12 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte "price": 2999, "version": 2, } - updateInput, err := basenumber.UpdateItemInputWithCondition( "conditional-test", 1640995777, updates, conditionExpr, conditionNames, conditionValues, ) require.NoError(t, err, "Should create conditional update with raw method") assert.NotNil(t, updateInput.ConditionExpression, "Should have condition expression") - t.Logf("✅ Raw conditional operations work correctly") }) } @@ -384,7 +360,6 @@ func testNumberInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte // ==================== Number QueryBuilder Tests ==================== func testNumberQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.Context) { - // Setup test data setupNumberTestData(t, client, ctx) t.Run("number_hash_key_query", func(t *testing.T) { @@ -394,7 +369,6 @@ func testNumberQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C require.NoError(t, err, "Should build number hash key query") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") assert.Equal(t, basenumber.TableName, *queryInput.TableName, "Should target correct table") - t.Logf("✅ Number hash key query built successfully") }) @@ -406,7 +380,6 @@ func testNumberQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build number hash+range query") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ Number hash+range query built successfully") }) @@ -419,7 +392,6 @@ func testNumberQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build query with number filters") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ Number filters query built successfully") }) @@ -431,7 +403,6 @@ func testNumberQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build number between query") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ Number range condition built successfully") }) @@ -448,7 +419,6 @@ func testNumberQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C assert.IsType(t, 0, item.Count, "Count should be int type") assert.IsType(t, 0, item.Price, "Price should be int type") } - t.Logf("✅ Number query execution returned %d items", len(items)) }) @@ -473,7 +443,6 @@ func testNumberQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C assert.LessOrEqual(t, itemsAsc[0].Timestamp, itemsAsc[1].Timestamp, "Ascending should be in increasing order") } } - t.Logf("✅ Number sorting works correctly") }) } @@ -489,7 +458,6 @@ func testNumberScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Co scanInput, err := sb.BuildScan() require.NoError(t, err, "Should build scan with number filters") assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") - t.Logf("✅ Number scan filters built successfully") }) @@ -502,7 +470,6 @@ func testNumberScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Co scanInput, err := sb.BuildScan() require.NoError(t, err, "Should build scan with advanced number filters") assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") - t.Logf("✅ Advanced number filters built successfully") }) @@ -517,7 +484,6 @@ func testNumberScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Co for _, item := range items { assert.Greater(t, item.Count, 20, "Items should match greater than filter") } - t.Logf("✅ Number scan execution returned %d items", len(items)) }) } @@ -541,7 +507,6 @@ func testNumberRangeConditions(t *testing.T, client *dynamodb.Client, ctx contex assert.GreaterOrEqual(t, item.Timestamp, 1640995200, "Timestamp should be >= start") assert.LessOrEqual(t, item.Timestamp, 1640995400, "Timestamp should be <= end") } - t.Logf("✅ Timestamp between condition returned %d items", len(items)) }) @@ -559,7 +524,6 @@ func testNumberRangeConditions(t *testing.T, client *dynamodb.Client, ctx contex for _, item := range items { assert.Greater(t, item.Timestamp, 1640995300, "Timestamp should be > threshold") } - t.Logf("✅ Timestamp greater than condition returned %d items", len(items)) }) @@ -577,7 +541,6 @@ func testNumberRangeConditions(t *testing.T, client *dynamodb.Client, ctx contex for _, item := range items { assert.Less(t, item.Timestamp, 1640995350, "Timestamp should be < threshold") } - t.Logf("✅ Timestamp less than condition returned %d items", len(items)) }) @@ -602,7 +565,6 @@ func testNumberRangeConditions(t *testing.T, client *dynamodb.Client, ctx contex _, err = qbLess.BuildQuery() require.NoError(t, err, "Should build count less than query") - t.Logf("✅ Count range conditions built successfully") }) } @@ -610,14 +572,12 @@ func testNumberRangeConditions(t *testing.T, client *dynamodb.Client, ctx contex // ==================== Number Increment Operations Tests ==================== func testNumberIncrementOperations(t *testing.T, client *dynamodb.Client, ctx context.Context) { - // Setup item for increment testing testItem := basenumber.SchemaItem{ Id: "increment-test", Timestamp: 1640995999, Count: 10, Price: 100, } - av, err := basenumber.ItemInput(testItem) require.NoError(t, err, "Should create test item for increment") @@ -640,9 +600,7 @@ func testNumberIncrementOperations(t *testing.T, client *dynamodb.Client, ctx co Key: key, }) require.NoError(t, err, "Should retrieve incremented item") - assert.Equal(t, "15", getResult.Item[basenumber.ColumnCount].(*types.AttributeValueMemberN).Value) - t.Logf("✅ Count incremented successfully: 10 + 5 = 15") }) @@ -659,9 +617,7 @@ func testNumberIncrementOperations(t *testing.T, client *dynamodb.Client, ctx co Key: key, }) require.NoError(t, err, "Should retrieve decremented item") - assert.Equal(t, "75", getResult.Item[basenumber.ColumnPrice].(*types.AttributeValueMemberN).Value) - t.Logf("✅ Price decremented successfully: 100 - 25 = 75") }) } @@ -672,16 +628,14 @@ func testNumberSchema(t *testing.T) { t.Run("number_table_schema", func(t *testing.T) { schema := basenumber.TableSchema - assert.Equal(t, "base-number", schema.TableName, "Table name should match") + assert.Equal(t, "base-number-all", schema.TableName, "Table name should match") assert.Equal(t, "id", schema.HashKey, "Hash key should be 'id'") assert.Equal(t, "timestamp", schema.RangeKey, "Range key should be 'timestamp'") assert.Len(t, schema.SecondaryIndexes, 0, "Should have no secondary indexes") - t.Logf("✅ Number schema structure validated") }) t.Run("number_attributes", func(t *testing.T) { - // Check primary attributes expectedPrimary := map[string]string{ "id": "S", // hash key is string "timestamp": "N", // range key is number @@ -692,29 +646,24 @@ func testNumberSchema(t *testing.T) { assert.True(t, exists, "Primary attribute %s should be expected", attr.Name) assert.Equal(t, expectedType, attr.Type, "Attribute %s should have correct type", attr.Name) } - - // Check common attributes (all number type) expectedCommon := map[string]string{ "count": "N", "price": "N", } - for _, attr := range basenumber.TableSchema.CommonAttributes { expectedType, exists := expectedCommon[attr.Name] assert.True(t, exists, "Common attribute %s should be expected", attr.Name) assert.Equal(t, expectedType, attr.Type, "Attribute %s should be number type", attr.Name) } - t.Logf("✅ Number attributes validated") }) t.Run("number_constants", func(t *testing.T) { - assert.Equal(t, "base-number", basenumber.TableName, "TableName constant should be correct") + assert.Equal(t, "base-number-all", basenumber.TableName, "TableName constant should be correct") assert.Equal(t, "id", basenumber.ColumnId, "ColumnId should be correct") assert.Equal(t, "timestamp", basenumber.ColumnTimestamp, "ColumnTimestamp should be correct") assert.Equal(t, "count", basenumber.ColumnCount, "ColumnCount should be correct") assert.Equal(t, "price", basenumber.ColumnPrice, "ColumnPrice should be correct") - t.Logf("✅ Number constants validated") }) @@ -742,7 +691,6 @@ func setupNumberTestData(t *testing.T, client *dynamodb.Client, ctx context.Cont {Id: "query-number-test", Timestamp: 1640995400, Count: 35, Price: 2000}, {Id: "query-number-test", Timestamp: 1640995500, Count: 45, Price: 2500}, } - for _, item := range testItems { av, err := basenumber.ItemInput(item) require.NoError(t, err, "Should marshal number test item") @@ -753,6 +701,5 @@ func setupNumberTestData(t *testing.T, client *dynamodb.Client, ctx context.Cont }) require.NoError(t, err, "Should store number test item") } - t.Logf("Setup complete: inserted %d number test items", len(testItems)) } diff --git a/tests/localstack/base_number_min_test.go b/tests/localstack/base_number_min_test.go new file mode 100644 index 0000000..8121e5c --- /dev/null +++ b/tests/localstack/base_number_min_test.go @@ -0,0 +1,432 @@ +package localstack + +import ( + "context" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + basenumbermin "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basenumbermin" +) + +// TestBaseNumberMIN focuses on Number (N) type operations with minimal generated code. +// This test validates numeric functionality using only basic methods (no sugar methods). +// +// Test Coverage: +// - Basic Number CRUD operations using universal methods +// - Number marshaling/unmarshaling +// - Core Query and Scan operations using .With() and .Filter() +// - Basic increment operations +// - Schema validation +// +// Schema: base-number__min.json +// - Table: "base-number-min" +// - Hash Key: id (S) +// - Range Key: timestamp (N) +// - Common: count (N), price (N) +// +// Note: MIN mode only includes universal methods like .With() and .Filter() +// No convenience methods like .WithEQ(), .FilterEQ(), .WithBetween() etc. +func TestBaseNumberMIN(t *testing.T) { + client := ConnectToLocalStack(t, DefaultLocalStackConfig()) + ctx, cancel := TestContext(3 * time.Minute) + defer cancel() + + t.Logf("Testing Number MIN operations on: %s", basenumbermin.TableName) + + t.Run("Number_MIN_BasicCRUD", func(t *testing.T) { + testNumberMINBasicCRUD(t, client, ctx) + }) + + t.Run("Number_MIN_QueryBuilder", func(t *testing.T) { + testNumberMINQueryBuilder(t, client, ctx) + }) + + t.Run("Number_MIN_ScanBuilder", func(t *testing.T) { + testNumberMINScanBuilder(t, client, ctx) + }) + + t.Run("Number_MIN_IncrementOperations", func(t *testing.T) { + testNumberMINIncrementOperations(t, client, ctx) + }) + + t.Run("Number_MIN_Schema", func(t *testing.T) { + t.Parallel() + testNumberMINSchema(t) + }) +} + +// ==================== Number MIN Basic CRUD ==================== + +func testNumberMINBasicCRUD(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Run("min_create_and_read", func(t *testing.T) { + item := basenumbermin.SchemaItem{ + Id: "min-number-001", + Timestamp: 1640995200, + Count: 42, + Price: 1999, + } + + av, err := basenumbermin.ItemInput(item) + require.NoError(t, err, "Should marshal number item in MIN mode") + assert.NotEmpty(t, av, "Marshaled item should not be empty") + + assert.IsType(t, &types.AttributeValueMemberS{}, av[basenumbermin.ColumnId]) + assert.IsType(t, &types.AttributeValueMemberN{}, av[basenumbermin.ColumnTimestamp]) + assert.IsType(t, &types.AttributeValueMemberN{}, av[basenumbermin.ColumnCount]) + assert.IsType(t, &types.AttributeValueMemberN{}, av[basenumbermin.ColumnPrice]) + + _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(basenumbermin.TableName), + Item: av, + }) + require.NoError(t, err, "Should store number item in DynamoDB") + + key, err := basenumbermin.KeyInput(item) + require.NoError(t, err, "Should create key from item") + + getResult, err := client.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(basenumbermin.TableName), + Key: key, + }) + require.NoError(t, err, "Should retrieve number item") + assert.NotEmpty(t, getResult.Item, "Retrieved item should not be empty") + + assert.Equal(t, "42", getResult.Item[basenumbermin.ColumnCount].(*types.AttributeValueMemberN).Value) + assert.Equal(t, "1999", getResult.Item[basenumbermin.ColumnPrice].(*types.AttributeValueMemberN).Value) + assert.Equal(t, "1640995200", getResult.Item[basenumbermin.ColumnTimestamp].(*types.AttributeValueMemberN).Value) + + t.Logf("✅ MIN mode basic number CRUD operations work correctly") + }) + + t.Run("min_raw_operations", func(t *testing.T) { + key, err := basenumbermin.KeyInputFromRaw("min-raw-001", 1640995300) + require.NoError(t, err, "Should create key from raw values") + assert.NotEmpty(t, key, "Raw key should not be empty") + + updates := map[string]any{ + "count": 100, + "price": 2500, + } + + updateInput, err := basenumbermin.UpdateItemInputFromRaw("min-raw-001", 1640995300, updates) + require.NoError(t, err, "Should create update input from raw values") + assert.NotNil(t, updateInput, "Update input should be created") + + t.Logf("✅ MIN mode raw number operations work correctly") + }) + + t.Run("min_number_edge_cases", func(t *testing.T) { + edgeCases := []basenumbermin.SchemaItem{ + {Id: "min-edge-1", Timestamp: 0, Count: 0, Price: 0}, + {Id: "min-edge-2", Timestamp: 1, Count: -100, Price: -50}, + {Id: "min-edge-3", Timestamp: 9999999999, Count: 2147483647, Price: 999999999}, + } + + for _, item := range edgeCases { + av, err := basenumbermin.ItemInput(item) + require.NoError(t, err, "Should handle number edge case: %s", item.Id) + assert.NotEmpty(t, av, "Marshaled edge case should not be empty") + } + + t.Logf("✅ MIN mode number edge cases handled successfully") + }) +} + +// ==================== Number MIN QueryBuilder Tests ==================== + +func testNumberMINQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.Context) { + setupNumberMINTestData(t, client, ctx) + + t.Run("min_query_universal_methods", func(t *testing.T) { + qb := basenumbermin.NewQueryBuilder(). + With("id", basenumbermin.EQ, "min-query-test") + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build query using universal .With() method") + assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") + + t.Logf("✅ MIN mode universal .With() method works") + }) + + t.Run("min_query_range_conditions", func(t *testing.T) { + qb := basenumbermin.NewQueryBuilder(). + With("id", basenumbermin.EQ, "min-query-test"). + With("timestamp", basenumbermin.BETWEEN, 1640995200, 1640995400) + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build range query using universal operators") + assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") + + t.Logf("✅ MIN mode range conditions work with universal operators") + }) + + t.Run("min_query_numeric_comparisons", func(t *testing.T) { + qbGT := basenumbermin.NewQueryBuilder(). + With("id", basenumbermin.EQ, "min-query-test"). + With("timestamp", basenumbermin.GT, 1640995300) + + queryInput, err := qbGT.BuildQuery() + require.NoError(t, err, "Should build GT query using universal operators") + assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") + + qbLT := basenumbermin.NewQueryBuilder(). + With("id", basenumbermin.EQ, "min-query-test"). + With("timestamp", basenumbermin.LT, 1640995350) + + queryInput, err = qbLT.BuildQuery() + require.NoError(t, err, "Should build LT query using universal operators") + assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") + + t.Logf("✅ MIN mode numeric comparisons work with universal operators") + }) + + t.Run("min_query_with_filters", func(t *testing.T) { + qb := basenumbermin.NewQueryBuilder(). + With("id", basenumbermin.EQ, "min-query-test") + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build basic query first") + assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") + + qbWithFilters := basenumbermin.NewQueryBuilder(). + With("id", basenumbermin.EQ, "min-query-test"). + Filter("count", basenumbermin.GT, 20) + + queryInputWithFilters, err := qbWithFilters.BuildQuery() + require.NoError(t, err, "Should build query with universal .Filter() method") + assert.NotNil(t, queryInputWithFilters.FilterExpression, "Should have filter expression") + + t.Logf("✅ MIN mode universal .Filter() method works for numbers") + }) + + t.Run("min_query_execution", func(t *testing.T) { + qb := basenumbermin.NewQueryBuilder(). + With("id", basenumbermin.EQ, "min-query-test") + + items, err := qb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN mode query") + assert.NotEmpty(t, items, "Should return items") + + for _, item := range items { + assert.Equal(t, "min-query-test", item.Id, "All items should have correct hash key") + assert.Greater(t, item.Timestamp, 0, "All items should have positive timestamp") + } + t.Logf("✅ MIN mode query execution returned %d items", len(items)) + }) + + t.Run("min_query_sorting", func(t *testing.T) { + qbAsc := basenumbermin.NewQueryBuilder(). + With("id", basenumbermin.EQ, "min-query-test"). + OrderByAsc() + + itemsAsc, err := qbAsc.Execute(ctx, client) + require.NoError(t, err, "Should execute ascending query") + + qbDesc := basenumbermin.NewQueryBuilder(). + With("id", basenumbermin.EQ, "min-query-test"). + OrderByDesc() + + itemsDesc, err := qbDesc.Execute(ctx, client) + require.NoError(t, err, "Should execute descending query") + + if len(itemsAsc) > 1 && len(itemsDesc) > 1 { + assert.NotEqual(t, itemsAsc[0].Timestamp, itemsDesc[0].Timestamp, "Sorting should produce different order") + } + t.Logf("✅ MIN mode sorting works correctly") + }) +} + +// ==================== Number MIN ScanBuilder Tests ==================== + +func testNumberMINScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Run("min_scan_universal_filter", func(t *testing.T) { + sb := basenumbermin.NewScanBuilder(). + Filter("count", basenumbermin.GT, 20) + + scanInput, err := sb.BuildScan() + require.NoError(t, err, "Should build scan with universal .Filter() method") + assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") + + t.Logf("✅ MIN mode scan universal .Filter() method works") + }) + + t.Run("min_scan_multiple_numeric_filters", func(t *testing.T) { + sb := basenumbermin.NewScanBuilder(). + Filter("count", basenumbermin.GT, 20). + Filter("price", basenumbermin.LT, 3000). + Filter("timestamp", basenumbermin.BETWEEN, 1640995200, 1640995500) + + scanInput, err := sb.BuildScan() + require.NoError(t, err, "Should build scan with multiple universal numeric filters") + assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") + t.Logf("✅ MIN mode multiple universal numeric filters work") + }) + + t.Run("min_scan_execution", func(t *testing.T) { + sb := basenumbermin.NewScanBuilder(). + Filter("id", basenumbermin.EQ, "min-query-test"). + Limit(5) + + items, err := sb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN mode scan") + + for _, item := range items { + assert.Equal(t, "min-query-test", item.Id, "Items should match filter") + } + t.Logf("✅ MIN mode scan execution returned %d items", len(items)) + }) +} + +// ==================== Number MIN Increment Operations Tests ==================== + +func testNumberMINIncrementOperations(t *testing.T, client *dynamodb.Client, ctx context.Context) { + // Setup item for increment testing + testItem := basenumbermin.SchemaItem{ + Id: "min-increment-test", + Timestamp: 1640995888, + Count: 10, + Price: 100, + } + + av, err := basenumbermin.ItemInput(testItem) + require.NoError(t, err, "Should create test item for increment") + + _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(basenumbermin.TableName), + Item: av, + }) + require.NoError(t, err, "Should store test item") + + t.Run("min_increment_basic", func(t *testing.T) { + incrementInput, err := basenumbermin.IncrementAttribute("min-increment-test", 1640995888, "count", 5) + require.NoError(t, err, "Should create increment input") + assert.NotNil(t, incrementInput.UpdateExpression, "Should have update expression") + assert.Contains(t, *incrementInput.UpdateExpression, "ADD", "Should use ADD operation") + + _, err = client.UpdateItem(ctx, incrementInput) + require.NoError(t, err, "Should increment count") + + key, _ := basenumbermin.KeyInputFromRaw("min-increment-test", 1640995888) + getResult, err := client.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(basenumbermin.TableName), + Key: key, + }) + require.NoError(t, err, "Should retrieve incremented item") + + assert.Equal(t, "15", getResult.Item[basenumbermin.ColumnCount].(*types.AttributeValueMemberN).Value) + + t.Logf("✅ MIN mode increment: 10 + 5 = 15") + }) + + t.Run("min_decrement_basic", func(t *testing.T) { + decrementInput, err := basenumbermin.IncrementAttribute("min-increment-test", 1640995888, "price", -25) + require.NoError(t, err, "Should create decrement input") + + _, err = client.UpdateItem(ctx, decrementInput) + require.NoError(t, err, "Should decrement price") + + key, _ := basenumbermin.KeyInputFromRaw("min-increment-test", 1640995888) + getResult, err := client.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(basenumbermin.TableName), + Key: key, + }) + require.NoError(t, err, "Should retrieve decremented item") + assert.Equal(t, "75", getResult.Item[basenumbermin.ColumnPrice].(*types.AttributeValueMemberN).Value) + t.Logf("✅ MIN mode decrement: 100 - 25 = 75") + }) +} + +// ==================== Number MIN Schema Tests ==================== + +func testNumberMINSchema(t *testing.T) { + t.Run("min_schema_structure", func(t *testing.T) { + schema := basenumbermin.TableSchema + + assert.Equal(t, "base-number-min", schema.TableName, "Table name should match MIN schema") + assert.Equal(t, "id", schema.HashKey, "Hash key should be 'id'") + assert.Equal(t, "timestamp", schema.RangeKey, "Range key should be 'timestamp'") + assert.Len(t, schema.SecondaryIndexes, 0, "Should have no secondary indexes") + + t.Logf("✅ MIN mode schema structure validated") + }) + + t.Run("min_constants", func(t *testing.T) { + assert.Equal(t, "base-number-min", basenumbermin.TableName, "TableName constant should be correct") + assert.Equal(t, "id", basenumbermin.ColumnId, "ColumnId should be correct") + assert.Equal(t, "timestamp", basenumbermin.ColumnTimestamp, "ColumnTimestamp should be correct") + assert.Equal(t, "count", basenumbermin.ColumnCount, "ColumnCount should be correct") + assert.Equal(t, "price", basenumbermin.ColumnPrice, "ColumnPrice should be correct") + + t.Logf("✅ MIN mode constants validated") + }) + + t.Run("min_numeric_operators_available", func(t *testing.T) { + assert.NotNil(t, basenumbermin.EQ, "EQ operator should be available") + assert.NotNil(t, basenumbermin.GT, "GT operator should be available") + assert.NotNil(t, basenumbermin.LT, "LT operator should be available") + assert.NotNil(t, basenumbermin.GTE, "GTE operator should be available") + assert.NotNil(t, basenumbermin.LTE, "LTE operator should be available") + assert.NotNil(t, basenumbermin.BETWEEN, "BETWEEN operator should be available") + t.Logf("✅ MIN mode universal numeric operators available") + }) + + t.Run("min_number_attributes", func(t *testing.T) { + expectedPrimary := map[string]string{ + "id": "S", + "timestamp": "N", + } + for _, attr := range basenumbermin.TableSchema.Attributes { + expectedType, exists := expectedPrimary[attr.Name] + assert.True(t, exists, "Primary attribute %s should be expected", attr.Name) + assert.Equal(t, expectedType, attr.Type, "Attribute %s should have correct type", attr.Name) + } + expectedCommon := map[string]string{ + "count": "N", + "price": "N", + } + for _, attr := range basenumbermin.TableSchema.CommonAttributes { + expectedType, exists := expectedCommon[attr.Name] + assert.True(t, exists, "Common attribute %s should be expected", attr.Name) + assert.Equal(t, expectedType, attr.Type, "Attribute %s should be number type", attr.Name) + } + t.Logf("✅ MIN mode number attributes validated") + }) + + t.Run("min_no_sugar_methods", func(t *testing.T) { + qb := basenumbermin.NewQueryBuilder() + assert.NotNil(t, qb, "QueryBuilder should be available") + + sb := basenumbermin.NewScanBuilder() + assert.NotNil(t, sb, "ScanBuilder should be available") + t.Logf("✅ MIN mode builders available (sugar methods should be absent)") + }) +} + +// ==================== Helper Functions ==================== + +func setupNumberMINTestData(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Helper() + + testItems := []basenumbermin.SchemaItem{ + {Id: "min-query-test", Timestamp: 1640995300, Count: 25, Price: 1500}, + {Id: "min-query-test", Timestamp: 1640995400, Count: 35, Price: 2000}, + {Id: "min-query-test", Timestamp: 1640995500, Count: 45, Price: 2500}, + } + for _, item := range testItems { + av, err := basenumbermin.ItemInput(item) + require.NoError(t, err, "Should marshal MIN test item") + + _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(basenumbermin.TableName), + Item: av, + }) + require.NoError(t, err, "Should store MIN test item") + } + t.Logf("MIN setup complete: inserted %d number test items", len(testItems)) +} diff --git a/tests/localstack/base_set_number.go b/tests/localstack/base_set_number_all.go similarity index 95% rename from tests/localstack/base_set_number.go rename to tests/localstack/base_set_number_all.go index 1d35854..7f4d3cb 100644 --- a/tests/localstack/base_set_number.go +++ b/tests/localstack/base_set_number_all.go @@ -2,6 +2,7 @@ package localstack import ( "context" + "maps" "testing" "time" @@ -11,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - basesetnumber "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basesetnumber" + basesetnumber "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basesetnumberall" ) // TestBaseSetNumber focuses on Number Set (NS) type operations and functionality. @@ -25,8 +26,8 @@ import ( // - Set operations in Query and Scan // - Edge cases (empty sets, large numbers, negative numbers) // -// Schema: base-set-number.json -// - Table: "base-set-number" +// Schema: base-set-number__all.json +// - Table: "base-set-number-all" // - Hash Key: user_id (S) // - Range Key: session_id (S) // - Common: scores (NS), ratings (NS) @@ -73,7 +74,6 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte Scores: []int{85, 92, 78, 96, 88}, Ratings: []int{4, 5, 3, 5, 4}, } - av, err := basesetnumber.ItemInput(item) require.NoError(t, err, "Should marshal number set item") assert.NotEmpty(t, av, "Marshaled item should not be empty") @@ -105,8 +105,8 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte TableName: aws.String(basesetnumber.TableName), Item: av, }) + t.Logf("Saved item attributes: %+v", av) require.NoError(t, err, "Should store number set item in DynamoDB") - t.Logf("✅ Created number set item: %s/%s", item.UserId, item.SessionId) }) @@ -123,13 +123,14 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte TableName: aws.String(basesetnumber.TableName), Key: key, }) + t.Logf("Retrieved item: %+v", getResult.Item) + t.Logf("Available keys: %v", maps.Keys(getResult.Item)) require.NoError(t, err, "Should retrieve number set item") assert.NotEmpty(t, getResult.Item, "Retrieved item should not be empty") assert.Equal(t, "user-001", getResult.Item[basesetnumber.ColumnUserId].(*types.AttributeValueMemberS).Value) assert.Equal(t, "session-2024-001", getResult.Item[basesetnumber.ColumnSessionId].(*types.AttributeValueMemberS).Value) - // Verify number sets scoresSet := getResult.Item[basesetnumber.ColumnScores].(*types.AttributeValueMemberNS) assert.Len(t, scoresSet.Value, 5, "Scores set should have 5 elements") assert.Contains(t, scoresSet.Value, "85", "Scores should contain 85") @@ -137,7 +138,6 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte ratingsSet := getResult.Item[basesetnumber.ColumnRatings].(*types.AttributeValueMemberNS) assert.Len(t, ratingsSet.Value, 5, "Ratings set should have 5 elements") assert.Contains(t, ratingsSet.Value, "4", "Ratings should contain 4") - t.Logf("✅ Retrieved number set item successfully") }) @@ -148,7 +148,6 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte Scores: []int{90, 95, 88, 92}, Ratings: []int{5, 4, 5}, } - updateInput, err := basesetnumber.UpdateItemInput(item) require.NoError(t, err, "Should create update input from item") @@ -162,7 +161,6 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte }) require.NoError(t, err, "Should retrieve updated item") - // Verify updated number sets scoresSet := getResult.Item[basesetnumber.ColumnScores].(*types.AttributeValueMemberNS) assert.Len(t, scoresSet.Value, 4, "Scores set should have 4 elements after update") assert.Contains(t, scoresSet.Value, "90", "Scores should contain 90") @@ -172,7 +170,6 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte assert.Len(t, ratingsSet.Value, 3, "Ratings set should have 3 elements after update") assert.Contains(t, ratingsSet.Value, "5", "Ratings should contain 5") assert.NotContains(t, ratingsSet.Value, "3", "Ratings should not contain 3") - t.Logf("✅ Updated number set item successfully") }) @@ -181,7 +178,6 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte UserId: "user-001", SessionId: "session-2024-001", } - deleteInput, err := basesetnumber.DeleteItemInput(item) require.NoError(t, err, "Should create delete input from item") @@ -195,7 +191,6 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte }) require.NoError(t, err, "GetItem should not error for missing item") assert.Empty(t, getResult.Item, "Number set item should be deleted") - t.Logf("✅ Deleted number set item successfully") }) @@ -204,23 +199,22 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte { UserId: "edge-1", SessionId: "zero-test", - Scores: []int{0}, // Zero value + Scores: []int{0}, Ratings: []int{0, 1, 2}, }, { UserId: "edge-2", SessionId: "negative-test", - Scores: []int{-100, -50, 0, 50, 100}, // Negative numbers + Scores: []int{-100, -50, 0, 50, 100}, Ratings: []int{1, 2, 3, 4, 5}, }, { UserId: "edge-3", SessionId: "large-test", - Scores: []int{999999, 1000000, 2147483647}, // Large numbers - Ratings: []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100}, // Large set + Scores: []int{999999, 1000000, 2147483647}, + Ratings: []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100}, }, } - for _, item := range edgeCases { av, err := basesetnumber.ItemInput(item) require.NoError(t, err, "Should handle number set edge case: %s", item.UserId) @@ -231,7 +225,6 @@ func testNumberSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte }) require.NoError(t, err, "Should store number set edge case item: %s", item.UserId) } - t.Logf("✅ Number set edge cases handled successfully") }) } @@ -246,7 +239,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co Scores: []int{75, 82, 89, 91}, Ratings: []int{3, 4, 4, 5}, } - av, err := basesetnumber.ItemInput(item) require.NoError(t, err, "Should marshal number set item") assert.NotEmpty(t, av, "Marshaled item should not be empty") @@ -256,7 +248,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co Item: av, }) require.NoError(t, err, "Should store number set item in DynamoDB") - t.Logf("✅ Created number set item for raw testing: %s/%s", item.UserId, item.SessionId) }) @@ -274,7 +265,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co assert.Equal(t, "user-raw-001", getResult.Item[basesetnumber.ColumnUserId].(*types.AttributeValueMemberS).Value) assert.Equal(t, "session-raw-001", getResult.Item[basesetnumber.ColumnSessionId].(*types.AttributeValueMemberS).Value) - // Verify number sets scoresSet := getResult.Item[basesetnumber.ColumnScores].(*types.AttributeValueMemberNS) assert.Contains(t, scoresSet.Value, "75", "Scores should contain 75") assert.Contains(t, scoresSet.Value, "91", "Scores should contain 91") @@ -283,7 +273,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co }) t.Run("update_number_set_item_raw", func(t *testing.T) { - // Helper function to extract number values regardless of DynamoDB type (NS or L) extractNumberValues := func(attr types.AttributeValue) []string { switch v := attr.(type) { case *types.AttributeValueMemberNS: @@ -301,7 +290,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co return nil } } - updates := map[string]any{ "scores": []int{80, 85, 90, 95, 100}, "ratings": []int{4, 5, 5}, @@ -320,7 +308,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co }) require.NoError(t, err, "Should retrieve updated item") - // Use helper to extract values regardless of DynamoDB type (NS or L) scoresValues := extractNumberValues(getResult.Item[basesetnumber.ColumnScores]) assert.Contains(t, scoresValues, "80", "Scores should contain 80") assert.Contains(t, scoresValues, "100", "Scores should contain 100") @@ -328,7 +315,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co ratingsValues := extractNumberValues(getResult.Item[basesetnumber.ColumnRatings]) assert.Contains(t, ratingsValues, "4", "Ratings should contain 4") assert.Contains(t, ratingsValues, "5", "Ratings should contain 5") - t.Logf("✅ Updated number set item successfully using raw method") }) @@ -346,7 +332,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co }) require.NoError(t, err, "GetItem should not error for missing item") assert.Empty(t, getResult.Item, "Number set item should be deleted") - t.Logf("✅ Deleted number set item successfully using raw method") }) @@ -362,7 +347,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co require.NoError(t, err, "Should create key from object") assert.Equal(t, keyFromRaw, keyFromObject, "Raw and object-based keys should be identical") - t.Logf("✅ Raw and object-based number set methods produce identical results") }) @@ -383,7 +367,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co updates: map[string]any{"scores": []int{-100, -1, 0, 1, 100}, "ratings": []int{1, 2, 3}}, }, } - for _, edgeCase := range edgeCases { updateInput, err := basesetnumber.UpdateItemInputFromRaw(edgeCase.userId, edgeCase.sessionId, edgeCase.updates) require.NoError(t, err, "Should handle raw number set edge case: %s", edgeCase.userId) @@ -393,7 +376,6 @@ func testNumberSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co require.NoError(t, err, "Should create delete input for edge case: %s", edgeCase.userId) assert.NotNil(t, deleteInput, "Delete input should be created") } - t.Logf("✅ Raw number set edge cases handled successfully") }) } @@ -410,7 +392,6 @@ func testNumberSetQueryBuilder(t *testing.T, client *dynamodb.Client, ctx contex require.NoError(t, err, "Should build number set hash key query") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") assert.Equal(t, basesetnumber.TableName, *queryInput.TableName, "Should target correct table") - t.Logf("✅ Number set hash key query built successfully") }) @@ -423,7 +404,6 @@ func testNumberSetQueryBuilder(t *testing.T, client *dynamodb.Client, ctx contex queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build query with number set contains filters") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ Number set contains filters query built successfully") }) @@ -440,7 +420,6 @@ func testNumberSetQueryBuilder(t *testing.T, client *dynamodb.Client, ctx contex assert.IsType(t, []int{}, item.Scores, "Scores should be int slice type") assert.IsType(t, []int{}, item.Ratings, "Ratings should be int slice type") } - t.Logf("✅ Number set query execution returned %d items", len(items)) }) @@ -462,7 +441,6 @@ func testNumberSetQueryBuilder(t *testing.T, client *dynamodb.Client, ctx contex if len(itemsAsc) > 1 && len(itemsDesc) > 1 { assert.NotEqual(t, itemsAsc[0].SessionId, itemsDesc[0].SessionId, "Number set sorting should produce different order") } - t.Logf("✅ Number set sorting works correctly") }) } @@ -477,7 +455,6 @@ func testNumberSetScanBuilder(t *testing.T, client *dynamodb.Client, ctx context scanInput, err := sb.BuildScan() require.NoError(t, err, "Should build scan with number set contains filter") assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") - t.Logf("✅ Number set contains scan filter built successfully") }) @@ -489,7 +466,6 @@ func testNumberSetScanBuilder(t *testing.T, client *dynamodb.Client, ctx context scanInput, err := sb.BuildScan() require.NoError(t, err, "Should build scan with multiple number set contains filters") assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") - t.Logf("✅ Multiple number set contains filters built successfully") }) @@ -504,7 +480,6 @@ func testNumberSetScanBuilder(t *testing.T, client *dynamodb.Client, ctx context for _, item := range items { assert.Contains(t, item.Scores, 85, "Items should match contains filter") } - t.Logf("✅ Number set scan execution returned %d items", len(items)) }) } @@ -548,7 +523,6 @@ func testNumberSetOperations(t *testing.T, client *dynamodb.Client, ctx context. assert.Contains(t, scoresSet.Value, "20", "Should still contain initial value") assert.Contains(t, scoresSet.Value, "30", "Should contain added value") assert.Contains(t, scoresSet.Value, "40", "Should contain added value") - t.Logf("✅ Added to number set successfully") }) @@ -571,7 +545,6 @@ func testNumberSetOperations(t *testing.T, client *dynamodb.Client, ctx context. assert.Contains(t, scoresSet.Value, "40", "Should still contain added value") assert.NotContains(t, scoresSet.Value, "20", "Should not contain removed value") assert.NotContains(t, scoresSet.Value, "30", "Should not contain removed value") - t.Logf("✅ Removed from number set successfully") }) @@ -593,7 +566,6 @@ func testNumberSetOperations(t *testing.T, client *dynamodb.Client, ctx context. assert.Contains(t, ratingsSet.Value, "1", "Should still contain rating 1") assert.Contains(t, ratingsSet.Value, "2", "Should contain added rating 2") assert.Contains(t, ratingsSet.Value, "5", "Should contain added rating 5") - t.Logf("✅ Added to ratings set successfully") }) } @@ -604,11 +576,10 @@ func testNumberSetSchema(t *testing.T) { t.Run("number_set_table_schema", func(t *testing.T) { schema := basesetnumber.TableSchema - assert.Equal(t, "base-set-number", schema.TableName, "Table name should match") + assert.Equal(t, "base-set-number-all", schema.TableName, "Table name should match") assert.Equal(t, "user_id", schema.HashKey, "Hash key should be 'user_id'") assert.Equal(t, "session_id", schema.RangeKey, "Range key should be 'session_id'") assert.Len(t, schema.SecondaryIndexes, 0, "Should have no secondary indexes") - t.Logf("✅ Number set schema structure validated") }) @@ -616,34 +587,29 @@ func testNumberSetSchema(t *testing.T) { expectedPrimary := map[string]string{ "session_id": "S", } - for _, attr := range basesetnumber.TableSchema.Attributes { expectedType, exists := expectedPrimary[attr.Name] assert.True(t, exists, "Primary attribute %s should be expected", attr.Name) assert.Equal(t, expectedType, attr.Type, "Attribute %s should have correct type", attr.Name) } - expectedCommon := map[string]string{ "scores": "NS", "ratings": "NS", } - for _, attr := range basesetnumber.TableSchema.CommonAttributes { expectedType, exists := expectedCommon[attr.Name] assert.True(t, exists, "Common attribute %s should be expected", attr.Name) assert.Equal(t, expectedType, attr.Type, "Attribute %s should be number set type", attr.Name) } - t.Logf("✅ Number set attributes validated") }) t.Run("number_set_constants", func(t *testing.T) { - assert.Equal(t, "base-set-number", basesetnumber.TableName, "TableName constant should be correct") + assert.Equal(t, "base-set-number-all", basesetnumber.TableName, "TableName constant should be correct") assert.Equal(t, "user_id", basesetnumber.ColumnUserId, "ColumnUserId should be correct") assert.Equal(t, "session_id", basesetnumber.ColumnSessionId, "ColumnSessionId should be correct") assert.Equal(t, "scores", basesetnumber.ColumnScores, "ColumnScores should be correct") assert.Equal(t, "ratings", basesetnumber.ColumnRatings, "ColumnRatings should be correct") - t.Logf("✅ Number set constants validated") }) @@ -652,19 +618,14 @@ func testNumberSetSchema(t *testing.T) { expectedAttrs := []string{"user_id", "session_id", "scores", "ratings"} assert.Len(t, attrs, len(expectedAttrs), "Should have correct number of attributes") - for _, expected := range expectedAttrs { assert.Contains(t, attrs, expected, "AttributeNames should contain %s", expected) } - t.Logf("✅ Number set AttributeNames validated") }) t.Run("number_set_go_types", func(t *testing.T) { - // Verify that generated struct has correct Go types for number sets item := basesetnumber.SchemaItem{} - - // These should compile without type errors item.Scores = []int{1, 2, 3} item.Ratings = []int{4, 5} @@ -672,26 +633,21 @@ func testNumberSetSchema(t *testing.T) { assert.IsType(t, []int{}, item.Ratings, "Ratings should be []int type") assert.IsType(t, "", item.UserId, "UserId should be string type") assert.IsType(t, "", item.SessionId, "SessionId should be string type") - t.Logf("✅ Number set Go types validated") }) t.Run("number_set_edge_values", func(t *testing.T) { - // Test edge values that should be supported item := basesetnumber.SchemaItem{ UserId: "edge-test", SessionId: "edge-session", - Scores: []int{-2147483648, 0, 2147483647}, // int32 min, zero, max - Ratings: []int{-100, -1, 0, 1, 100}, // negative, zero, positive + Scores: []int{-2147483648, 0, 2147483647}, + Ratings: []int{-100, -1, 0, 1, 100}, } - - // Should compile and assign without issues assert.Len(t, item.Scores, 3, "Should handle edge score values") assert.Len(t, item.Ratings, 5, "Should handle edge rating values") assert.Contains(t, item.Scores, 0, "Should handle zero value") assert.Contains(t, item.Scores, -2147483648, "Should handle negative values") assert.Contains(t, item.Ratings, -100, "Should handle negative ratings") - t.Logf("✅ Number set edge values validated") }) } @@ -721,7 +677,6 @@ func setupNumberSetTestData(t *testing.T, client *dynamodb.Client, ctx context.C Ratings: []int{3, 4, 5, 5, 4}, }, } - for _, item := range testItems { av, err := basesetnumber.ItemInput(item) require.NoError(t, err, "Should marshal number set test item") @@ -732,6 +687,5 @@ func setupNumberSetTestData(t *testing.T, client *dynamodb.Client, ctx context.C }) require.NoError(t, err, "Should store number set test item") } - t.Logf("Setup complete: inserted %d number set test items", len(testItems)) } diff --git a/tests/localstack/base_set_string_test.go b/tests/localstack/base_set_string_all_test.go similarity index 97% rename from tests/localstack/base_set_string_test.go rename to tests/localstack/base_set_string_all_test.go index 8d1461d..cf31266 100644 --- a/tests/localstack/base_set_string_test.go +++ b/tests/localstack/base_set_string_all_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - basesetstring "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basesetstring" + basesetstring "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basesetstringall" ) // TestBaseSetString focuses on String Set (SS) type operations and functionality. @@ -25,8 +25,8 @@ import ( // - Set operations in Query and Scan // - Edge cases (empty sets, large sets, duplicates) // -// Schema: base-set-string.json -// - Table: "base-set-string" +// Schema: base-set-string__all.json +// - Table: "base-set-string-all" // - Hash Key: id (S) // - Range Key: group_id (S) // - Common: tags (SS), categories (SS) @@ -88,7 +88,6 @@ func testStringSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte assert.IsType(t, &types.AttributeValueMemberSS{}, av[basesetstring.ColumnTags]) assert.IsType(t, &types.AttributeValueMemberSS{}, av[basesetstring.ColumnCategories]) - // Verify string set values tagsSet := av[basesetstring.ColumnTags].(*types.AttributeValueMemberSS) assert.Len(t, tagsSet.Value, 3, "Tags set should have 3 elements") assert.Contains(t, tagsSet.Value, "javascript", "Tags should contain javascript") @@ -106,7 +105,6 @@ func testStringSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte Item: av, }) require.NoError(t, err, "Should store string set item in DynamoDB") - t.Logf("✅ Created string set item: %s/%s", item.Id, item.GroupId) }) @@ -129,7 +127,6 @@ func testStringSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte assert.Equal(t, "set-test-001", getResult.Item[basesetstring.ColumnId].(*types.AttributeValueMemberS).Value) assert.Equal(t, "web-development", getResult.Item[basesetstring.ColumnGroupId].(*types.AttributeValueMemberS).Value) - // Verify string sets tagsSet := getResult.Item[basesetstring.ColumnTags].(*types.AttributeValueMemberSS) assert.Len(t, tagsSet.Value, 3, "Tags set should have 3 elements") assert.Contains(t, tagsSet.Value, "javascript", "Tags should contain javascript") @@ -137,7 +134,6 @@ func testStringSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte categoriesSet := getResult.Item[basesetstring.ColumnCategories].(*types.AttributeValueMemberSS) assert.Len(t, categoriesSet.Value, 3, "Categories set should have 3 elements") assert.Contains(t, categoriesSet.Value, "frontend", "Categories should contain frontend") - t.Logf("✅ Retrieved string set item successfully") }) @@ -162,7 +158,6 @@ func testStringSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte }) require.NoError(t, err, "Should retrieve updated item") - // Verify updated string sets tagsSet := getResult.Item[basesetstring.ColumnTags].(*types.AttributeValueMemberSS) assert.Len(t, tagsSet.Value, 4, "Tags set should have 4 elements after update") assert.Contains(t, tagsSet.Value, "typescript", "Tags should contain typescript") @@ -171,7 +166,6 @@ func testStringSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte categoriesSet := getResult.Item[basesetstring.ColumnCategories].(*types.AttributeValueMemberSS) assert.Len(t, categoriesSet.Value, 2, "Categories set should have 2 elements after update") assert.NotContains(t, categoriesSet.Value, "fullstack", "Categories should not contain fullstack") - t.Logf("✅ Updated string set item successfully") }) @@ -194,7 +188,6 @@ func testStringSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte }) require.NoError(t, err, "GetItem should not error for missing item") assert.Empty(t, getResult.Item, "String set item should be deleted") - t.Logf("✅ Deleted string set item successfully") }) @@ -203,7 +196,7 @@ func testStringSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte { Id: "edge-1", GroupId: "empty-test", - Tags: []string{}, // Empty set + Tags: []string{}, Categories: []string{"single"}, }, { @@ -219,7 +212,6 @@ func testStringSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte Categories: []string{"category-with-very-long-name-that-tests-string-length-limits"}, }, } - for _, item := range edgeCases { av, err := basesetstring.ItemInput(item) require.NoError(t, err, "Should handle string set edge case: %s", item.Id) @@ -230,7 +222,6 @@ func testStringSetInput(t *testing.T, client *dynamodb.Client, ctx context.Conte }) require.NoError(t, err, "Should store string set edge case item: %s", item.Id) } - t.Logf("✅ String set edge cases handled successfully") }) } @@ -245,7 +236,6 @@ func testStringSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co Tags: []string{"swift", "kotlin", "react-native"}, Categories: []string{"ios", "android", "cross-platform"}, } - av, err := basesetstring.ItemInput(item) require.NoError(t, err, "Should marshal string set item") assert.NotEmpty(t, av, "Marshaled item should not be empty") @@ -255,7 +245,6 @@ func testStringSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co Item: av, }) require.NoError(t, err, "Should store string set item in DynamoDB") - t.Logf("✅ Created string set item for raw testing: %s/%s", item.Id, item.GroupId) }) @@ -273,11 +262,9 @@ func testStringSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co assert.Equal(t, "set-raw-001", getResult.Item[basesetstring.ColumnId].(*types.AttributeValueMemberS).Value) assert.Equal(t, "mobile-development", getResult.Item[basesetstring.ColumnGroupId].(*types.AttributeValueMemberS).Value) - // Verify string sets tagsSet := getResult.Item[basesetstring.ColumnTags].(*types.AttributeValueMemberSS) assert.Contains(t, tagsSet.Value, "swift", "Tags should contain swift") assert.Contains(t, tagsSet.Value, "kotlin", "Tags should contain kotlin") - t.Logf("✅ Retrieved string set item successfully using raw key") }) @@ -300,7 +287,6 @@ func testStringSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co }) require.NoError(t, err, "Should retrieve updated item") - // Verify updated sets tagsSet := getResult.Item[basesetstring.ColumnTags].(*types.AttributeValueMemberSS) assert.Contains(t, tagsSet.Value, "flutter", "Tags should contain flutter") assert.Contains(t, tagsSet.Value, "xamarin", "Tags should contain xamarin") @@ -308,7 +294,6 @@ func testStringSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co categoriesSet := getResult.Item[basesetstring.ColumnCategories].(*types.AttributeValueMemberSS) assert.Contains(t, categoriesSet.Value, "native", "Categories should contain native") assert.Contains(t, categoriesSet.Value, "hybrid", "Categories should contain hybrid") - t.Logf("✅ Updated string set item successfully using raw method") }) @@ -326,7 +311,6 @@ func testStringSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co }) require.NoError(t, err, "GetItem should not error for missing item") assert.Empty(t, getResult.Item, "String set item should be deleted") - t.Logf("✅ Deleted string set item successfully using raw method") }) @@ -342,7 +326,6 @@ func testStringSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co require.NoError(t, err, "Should create key from object") assert.Equal(t, keyFromRaw, keyFromObject, "Raw and object-based keys should be identical") - t.Logf("✅ Raw and object-based string set methods produce identical results") }) @@ -363,7 +346,6 @@ func testStringSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co updates: map[string]any{"tags": []string{"tag!@#", "спец-символы"}, "categories": []string{"CAPS", "lower"}}, }, } - for _, edgeCase := range edgeCases { updateInput, err := basesetstring.UpdateItemInputFromRaw(edgeCase.id, edgeCase.groupId, edgeCase.updates) require.NoError(t, err, "Should handle raw string set edge case: %s", edgeCase.id) @@ -373,7 +355,6 @@ func testStringSetInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Co require.NoError(t, err, "Should create delete input for edge case: %s", edgeCase.id) assert.NotNil(t, deleteInput, "Delete input should be created") } - t.Logf("✅ Raw string set edge cases handled successfully") }) } @@ -390,7 +371,6 @@ func testStringSetQueryBuilder(t *testing.T, client *dynamodb.Client, ctx contex require.NoError(t, err, "Should build string set hash key query") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") assert.Equal(t, basesetstring.TableName, *queryInput.TableName, "Should target correct table") - t.Logf("✅ String set hash key query built successfully") }) @@ -403,7 +383,6 @@ func testStringSetQueryBuilder(t *testing.T, client *dynamodb.Client, ctx contex queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build query with string set contains filters") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ String set contains filters query built successfully") }) @@ -420,7 +399,6 @@ func testStringSetQueryBuilder(t *testing.T, client *dynamodb.Client, ctx contex assert.IsType(t, []string{}, item.Tags, "Tags should be string slice type") assert.IsType(t, []string{}, item.Categories, "Categories should be string slice type") } - t.Logf("✅ String set query execution returned %d items", len(items)) }) @@ -442,7 +420,6 @@ func testStringSetQueryBuilder(t *testing.T, client *dynamodb.Client, ctx contex if len(itemsAsc) > 1 && len(itemsDesc) > 1 { assert.NotEqual(t, itemsAsc[0].GroupId, itemsDesc[0].GroupId, "String set sorting should produce different order") } - t.Logf("✅ String set sorting works correctly") }) } @@ -457,7 +434,6 @@ func testStringSetScanBuilder(t *testing.T, client *dynamodb.Client, ctx context scanInput, err := sb.BuildScan() require.NoError(t, err, "Should build scan with string set contains filter") assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") - t.Logf("✅ String set contains scan filter built successfully") }) @@ -469,7 +445,6 @@ func testStringSetScanBuilder(t *testing.T, client *dynamodb.Client, ctx context scanInput, err := sb.BuildScan() require.NoError(t, err, "Should build scan with multiple string set contains filters") assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") - t.Logf("✅ Multiple string set contains filters built successfully") }) @@ -484,7 +459,6 @@ func testStringSetScanBuilder(t *testing.T, client *dynamodb.Client, ctx context for _, item := range items { assert.Contains(t, item.Tags, "javascript", "Items should match contains filter") } - t.Logf("✅ String set scan execution returned %d items", len(items)) }) } @@ -492,7 +466,6 @@ func testStringSetScanBuilder(t *testing.T, client *dynamodb.Client, ctx context // ==================== String Set Operations Tests ==================== func testStringSetOperations(t *testing.T, client *dynamodb.Client, ctx context.Context) { - // Setup item for set operations testing testItem := basesetstring.SchemaItem{ Id: "set-ops-test", GroupId: "operations", @@ -573,7 +546,6 @@ func testStringSetOperations(t *testing.T, client *dynamodb.Client, ctx context. assert.Contains(t, categoriesSet.Value, "category1", "Should still contain category1") assert.Contains(t, categoriesSet.Value, "category2", "Should contain added category2") assert.Contains(t, categoriesSet.Value, "category3", "Should contain added category3") - t.Logf("✅ Added to categories set successfully") }) } @@ -584,11 +556,10 @@ func testStringSetSchema(t *testing.T) { t.Run("string_set_table_schema", func(t *testing.T) { schema := basesetstring.TableSchema - assert.Equal(t, "base-set-string", schema.TableName, "Table name should match") + assert.Equal(t, "base-set-string-all", schema.TableName, "Table name should match") assert.Equal(t, "id", schema.HashKey, "Hash key should be 'id'") assert.Equal(t, "group_id", schema.RangeKey, "Range key should be 'group_id'") assert.Len(t, schema.SecondaryIndexes, 0, "Should have no secondary indexes") - t.Logf("✅ String set schema structure validated") }) @@ -597,34 +568,29 @@ func testStringSetSchema(t *testing.T) { "id": "S", "group_id": "S", } - for _, attr := range basesetstring.TableSchema.Attributes { expectedType, exists := expectedPrimary[attr.Name] assert.True(t, exists, "Primary attribute %s should be expected", attr.Name) assert.Equal(t, expectedType, attr.Type, "Attribute %s should have correct type", attr.Name) } - expectedCommon := map[string]string{ "tags": "SS", "categories": "SS", } - for _, attr := range basesetstring.TableSchema.CommonAttributes { expectedType, exists := expectedCommon[attr.Name] assert.True(t, exists, "Common attribute %s should be expected", attr.Name) assert.Equal(t, expectedType, attr.Type, "Attribute %s should be string set type", attr.Name) } - t.Logf("✅ String set attributes validated") }) t.Run("string_set_constants", func(t *testing.T) { - assert.Equal(t, "base-set-string", basesetstring.TableName, "TableName constant should be correct") + assert.Equal(t, "base-set-string-all", basesetstring.TableName, "TableName constant should be correct") assert.Equal(t, "id", basesetstring.ColumnId, "ColumnId should be correct") assert.Equal(t, "group_id", basesetstring.ColumnGroupId, "ColumnGroupId should be correct") assert.Equal(t, "tags", basesetstring.ColumnTags, "ColumnTags should be correct") assert.Equal(t, "categories", basesetstring.ColumnCategories, "ColumnCategories should be correct") - t.Logf("✅ String set constants validated") }) @@ -633,19 +599,15 @@ func testStringSetSchema(t *testing.T) { expectedAttrs := []string{"id", "group_id", "tags", "categories"} assert.Len(t, attrs, len(expectedAttrs), "Should have correct number of attributes") - for _, expected := range expectedAttrs { assert.Contains(t, attrs, expected, "AttributeNames should contain %s", expected) } - t.Logf("✅ String set AttributeNames validated") }) t.Run("string_set_go_types", func(t *testing.T) { - // Verify that generated struct has correct Go types for string sets item := basesetstring.SchemaItem{} - // These should compile without type errors item.Tags = []string{"test1", "test2"} item.Categories = []string{"cat1", "cat2"} @@ -653,7 +615,6 @@ func testStringSetSchema(t *testing.T) { assert.IsType(t, []string{}, item.Categories, "Categories should be []string type") assert.IsType(t, "", item.Id, "Id should be string type") assert.IsType(t, "", item.GroupId, "GroupId should be string type") - t.Logf("✅ String set Go types validated") }) } @@ -683,7 +644,6 @@ func setupStringSetTestData(t *testing.T, client *dynamodb.Client, ctx context.C Categories: []string{"frontend", "backend", "fullstack"}, }, } - for _, item := range testItems { av, err := basesetstring.ItemInput(item) require.NoError(t, err, "Should marshal string set test item") @@ -694,6 +654,5 @@ func setupStringSetTestData(t *testing.T, client *dynamodb.Client, ctx context.C }) require.NoError(t, err, "Should store string set test item") } - t.Logf("Setup complete: inserted %d string set test items", len(testItems)) } diff --git a/tests/localstack/base_string_test.go b/tests/localstack/base_string_all_test.go similarity index 96% rename from tests/localstack/base_string_test.go rename to tests/localstack/base_string_all_test.go index 6f390bf..9d58896 100644 --- a/tests/localstack/base_string_test.go +++ b/tests/localstack/base_string_all_test.go @@ -11,9 +11,25 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - basestring "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basestring" + basestring "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basestringall" ) +// TestBaseString focuses on String (S) type operations and functionality. +// This test validates string-specific features without other data types. +// +// Test Coverage: +// - String CRUD operations +// - String marshaling/unmarshaling +// - String filter conditions (BeginsWith, Contains, NotContains) +// - String operations in Query and Scan +// - String comparison operations (GT, LT, Between) +// - Edge cases (empty strings, special characters, long strings) +// +// Schema: base-string__all.json +// - Table: "base-string-all" +// - Hash Key: id (S) +// - Range Key: category (S) +// - Common: title (S), description (S) func TestBaseString(t *testing.T) { client := ConnectToLocalStack(t, DefaultLocalStackConfig()) ctx, cancel := TestContext(3 * time.Minute) @@ -51,7 +67,6 @@ func testStringInput(t *testing.T, client *dynamodb.Client, ctx context.Context) Title: "String Operations Guide", Description: "Comprehensive guide for string handling", } - av, err := basestring.ItemInput(item) require.NoError(t, err, "Should marshal string item") assert.NotEmpty(t, av, "Marshaled item should not be empty") @@ -71,7 +86,6 @@ func testStringInput(t *testing.T, client *dynamodb.Client, ctx context.Context) Item: av, }) require.NoError(t, err, "Should store string item in DynamoDB") - t.Logf("✅ Created string item: %s/%s", item.Id, item.Category) }) @@ -80,7 +94,6 @@ func testStringInput(t *testing.T, client *dynamodb.Client, ctx context.Context) Id: "string-test-001", Category: "docs", } - key, err := basestring.KeyInput(item) require.NoError(t, err, "Should create key from item") @@ -95,7 +108,6 @@ func testStringInput(t *testing.T, client *dynamodb.Client, ctx context.Context) assert.Equal(t, "string-test-001", getResult.Item[basestring.ColumnId].(*types.AttributeValueMemberS).Value) assert.Equal(t, "String Operations Guide", getResult.Item[basestring.ColumnTitle].(*types.AttributeValueMemberS).Value) assert.Equal(t, "Comprehensive guide for string handling", getResult.Item[basestring.ColumnDescription].(*types.AttributeValueMemberS).Value) - t.Logf("✅ Retrieved string item successfully") }) @@ -106,7 +118,6 @@ func testStringInput(t *testing.T, client *dynamodb.Client, ctx context.Context) Title: "Updated String Guide", Description: "Updated comprehensive guide for string operations", } - updateInput, err := basestring.UpdateItemInput(item) require.NoError(t, err, "Should create update input from item") @@ -122,7 +133,6 @@ func testStringInput(t *testing.T, client *dynamodb.Client, ctx context.Context) assert.Equal(t, "Updated String Guide", getResult.Item[basestring.ColumnTitle].(*types.AttributeValueMemberS).Value) assert.Equal(t, "Updated comprehensive guide for string operations", getResult.Item[basestring.ColumnDescription].(*types.AttributeValueMemberS).Value) - t.Logf("✅ Updated string item successfully") }) @@ -145,7 +155,6 @@ func testStringInput(t *testing.T, client *dynamodb.Client, ctx context.Context) }) require.NoError(t, err, "GetItem should not error for missing item") assert.Empty(t, getResult.Item, "String item should be deleted") - t.Logf("✅ Deleted string item successfully") }) @@ -156,7 +165,6 @@ func testStringInput(t *testing.T, client *dynamodb.Client, ctx context.Context) {Id: "edge-3", Category: "long", Title: "Very " + string(make([]byte, 100)), Description: "Long string test"}, {Id: "edge-4", Category: "minimal", Title: "x", Description: "Single char"}, } - for _, item := range edgeCases { av, err := basestring.ItemInput(item) require.NoError(t, err, "Should handle edge case: %s", item.Id) @@ -167,7 +175,6 @@ func testStringInput(t *testing.T, client *dynamodb.Client, ctx context.Context) }) require.NoError(t, err, "Should store edge case item: %s", item.Id) } - t.Logf("✅ String edge cases handled successfully") }) } @@ -190,7 +197,6 @@ func testStringInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte Item: av, }) require.NoError(t, err, "Should store string item in DynamoDB") - t.Logf("✅ Created string item for raw testing: %s/%s", item.Id, item.Category) }) @@ -208,7 +214,6 @@ func testStringInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte assert.Equal(t, "string-raw-001", getResult.Item[basestring.ColumnId].(*types.AttributeValueMemberS).Value) assert.Equal(t, "raw-docs", getResult.Item[basestring.ColumnCategory].(*types.AttributeValueMemberS).Value) assert.Equal(t, "Raw String Operations Guide", getResult.Item[basestring.ColumnTitle].(*types.AttributeValueMemberS).Value) - t.Logf("✅ Retrieved string item successfully using raw key") }) @@ -233,7 +238,6 @@ func testStringInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte assert.Equal(t, "Updated Raw String Guide", getResult.Item[basestring.ColumnTitle].(*types.AttributeValueMemberS).Value) assert.Equal(t, "Updated guide for raw string operations methods", getResult.Item[basestring.ColumnDescription].(*types.AttributeValueMemberS).Value) - t.Logf("✅ Updated string item successfully using raw method") }) @@ -251,7 +255,6 @@ func testStringInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte }) require.NoError(t, err, "GetItem should not error for missing item") assert.Empty(t, getResult.Item, "String item should be deleted") - t.Logf("✅ Deleted string item successfully using raw method") }) @@ -265,9 +268,7 @@ func testStringInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte } keyFromObject, err := basestring.KeyInput(item) require.NoError(t, err, "Should create key from object") - assert.Equal(t, keyFromRaw, keyFromObject, "Raw and object-based keys should be identical") - t.Logf("✅ Raw and object-based methods produce identical results") }) @@ -293,7 +294,6 @@ func testStringInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte updates: map[string]any{"title": "Special: !@#$%^&*()", "description": "Special characters"}, }, } - for _, edgeCase := range edgeCases { updateInput, err := basestring.UpdateItemInputFromRaw(edgeCase.id, edgeCase.category, edgeCase.updates) require.NoError(t, err, "Should handle raw edge case: %s", edgeCase.id) @@ -303,7 +303,6 @@ func testStringInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte require.NoError(t, err, "Should create delete input for edge case: %s", edgeCase.id) assert.NotNil(t, deleteInput, "Delete input should be created") } - t.Logf("✅ Raw string edge cases handled successfully") }) @@ -326,14 +325,12 @@ func testStringInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Conte "title": "Conditional Update", "version": 2, } - updateInput, err := basestring.UpdateItemInputWithCondition( "conditional-test", "raw-condition", updates, conditionExpr, conditionNames, conditionValues, ) require.NoError(t, err, "Should create conditional update with raw method") assert.NotNil(t, updateInput.ConditionExpression, "Should have condition expression") - t.Logf("✅ Raw conditional operations work correctly") }) } @@ -348,7 +345,6 @@ func testStringQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C require.NoError(t, err, "Should build string hash key query") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") assert.Equal(t, basestring.TableName, *queryInput.TableName, "Should target correct table") - t.Logf("✅ String hash key query built successfully") }) @@ -360,7 +356,6 @@ func testStringQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build string hash+range query") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ String hash+range query built successfully") }) @@ -373,7 +368,6 @@ func testStringQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build query with string filters") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ String filters query built successfully") }) @@ -385,7 +379,6 @@ func testStringQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build string between query") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ String range condition built successfully") }) @@ -402,7 +395,6 @@ func testStringQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C assert.IsType(t, "", item.Title, "Title should be string type") assert.IsType(t, "", item.Description, "Description should be string type") } - t.Logf("✅ String query execution returned %d items", len(items)) }) @@ -424,7 +416,6 @@ func testStringQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.C if len(itemsAsc) > 1 && len(itemsDesc) > 1 { assert.NotEqual(t, itemsAsc[0].Category, itemsDesc[0].Category, "Sorting should produce different order") } - t.Logf("✅ String sorting works correctly") }) } @@ -438,7 +429,6 @@ func testStringScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Co scanInput, err := sb.BuildScan() require.NoError(t, err, "Should build scan with string filters") assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") - t.Logf("✅ String scan filters built successfully") }) @@ -448,7 +438,6 @@ func testStringScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Co scanInput, err := sb.BuildScan() require.NoError(t, err, "Should build scan with contains filter") assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") - t.Logf("✅ String contains filter built successfully") }) @@ -458,7 +447,6 @@ func testStringScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Co scanInput, err := sb.BuildScan() require.NoError(t, err, "Should build scan with begins_with filter") assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") - t.Logf("✅ String begins_with filter built successfully") }) @@ -471,7 +459,6 @@ func testStringScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Co scanInput, err := sb.BuildScan() require.NoError(t, err, "Should build scan with advanced string filters") assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") - t.Logf("✅ Advanced string filters built successfully") }) @@ -486,7 +473,6 @@ func testStringScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Co for _, item := range items { assert.Contains(t, item.Title, "API", "Items should match contains filter") } - t.Logf("✅ String scan execution returned %d items", len(items)) }) } @@ -495,7 +481,7 @@ func testStringSchema(t *testing.T) { t.Run("string_table_schema", func(t *testing.T) { schema := basestring.TableSchema - assert.Equal(t, "base-string", schema.TableName, "Table name should match") + assert.Equal(t, "base-string-all", schema.TableName, "Table name should match") assert.Equal(t, "id", schema.HashKey, "Hash key should be 'id'") assert.Equal(t, "category", schema.RangeKey, "Range key should be 'category'") assert.Len(t, schema.SecondaryIndexes, 0, "Should have no secondary indexes") @@ -508,47 +494,40 @@ func testStringSchema(t *testing.T) { "id": "S", "category": "S", } - for _, attr := range basestring.TableSchema.Attributes { expectedType, exists := expectedPrimary[attr.Name] assert.True(t, exists, "Primary attribute %s should be expected", attr.Name) assert.Equal(t, expectedType, attr.Type, "Attribute %s should be string type", attr.Name) } - expectedCommon := map[string]string{ "title": "S", "description": "S", } - for _, attr := range basestring.TableSchema.CommonAttributes { expectedType, exists := expectedCommon[attr.Name] assert.True(t, exists, "Common attribute %s should be expected", attr.Name) assert.Equal(t, expectedType, attr.Type, "Attribute %s should be string type", attr.Name) } - t.Logf("✅ String attributes validated") }) t.Run("string_constants", func(t *testing.T) { - assert.Equal(t, "base-string", basestring.TableName, "TableName constant should be correct") + assert.Equal(t, "base-string-all", basestring.TableName, "TableName constant should be correct") assert.Equal(t, "id", basestring.ColumnId, "ColumnId should be correct") assert.Equal(t, "category", basestring.ColumnCategory, "ColumnCategory should be correct") assert.Equal(t, "title", basestring.ColumnTitle, "ColumnTitle should be correct") assert.Equal(t, "description", basestring.ColumnDescription, "ColumnDescription should be correct") - t.Logf("✅ String constants validated") }) t.Run("string_attribute_names", func(t *testing.T) { attrs := basestring.AttributeNames expectedAttrs := []string{"id", "category", "title", "description"} - assert.Len(t, attrs, len(expectedAttrs), "Should have correct number of attributes") for _, expected := range expectedAttrs { assert.Contains(t, attrs, expected, "AttributeNames should contain %s", expected) } - t.Logf("✅ String AttributeNames validated") }) } @@ -561,7 +540,6 @@ func setupStringTestData(t *testing.T, client *dynamodb.Client, ctx context.Cont {Id: "query-string-test", Category: "sdk", Title: "SDK Reference", Description: "Complete SDK documentation"}, {Id: "query-string-test", Category: "tutorial", Title: "Getting Started", Description: "Quick start tutorial"}, } - for _, item := range testItems { av, err := basestring.ItemInput(item) require.NoError(t, err, "Should marshal string test item") @@ -572,6 +550,5 @@ func setupStringTestData(t *testing.T, client *dynamodb.Client, ctx context.Cont }) require.NoError(t, err, "Should store string test item") } - t.Logf("Setup complete: inserted %d string test items", len(testItems)) } diff --git a/tests/localstack/base_string_min_test.go b/tests/localstack/base_string_min_test.go new file mode 100644 index 0000000..df5f1e7 --- /dev/null +++ b/tests/localstack/base_string_min_test.go @@ -0,0 +1,386 @@ +package localstack + +import ( + "context" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + basestringmin "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/basestringmin" +) + +// TestBaseStringMIN focuses on String (S) type operations with minimal generated code. +// This test validates string functionality using only basic methods (no sugar methods). +// +// Test Coverage: +// - Basic String CRUD operations using universal methods +// - String marshaling/unmarshaling +// - Core Query and Scan operations using .With() and .Filter() +// - Schema validation +// +// Schema: base-string__min.json +// - Table: "base-string-min" +// - Hash Key: id (S) +// - Range Key: category (S) +// - Common: title (S), description (S) +// +// Note: MIN mode only includes universal methods like .With() and .Filter() +// No convenience methods like .WithEQ(), .FilterEQ(), .FilterContains() etc. +func TestBaseStringMin(t *testing.T) { + client := ConnectToLocalStack(t, DefaultLocalStackConfig()) + ctx, cancel := TestContext(3 * time.Minute) + defer cancel() + + t.Logf("Testing MIN mode String operations on: %s", basestringmin.TableName) + + t.Run("StringMIN_Input", func(t *testing.T) { + testStringMINInput(t, client, ctx) + }) + + t.Run("StringMIN_Input_Raw", func(t *testing.T) { + testStringMINInputRaw(t, client, ctx) + }) + + t.Run("StringMIN_QueryBuilder", func(t *testing.T) { + testStringMINQueryBuilder(t, client, ctx) + }) + + t.Run("StringMIN_ScanBuilder", func(t *testing.T) { + testStringMINScanBuilder(t, client, ctx) + }) + + t.Run("StringMIN_Schema", func(t *testing.T) { + t.Parallel() + testStringMINSchema(t) + }) +} + +// ==================== String MIN Object Input ==================== + +func testStringMINInput(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Run("min_basic_crud", func(t *testing.T) { + item := basestringmin.SchemaItem{ + Id: "min-string-001", + Category: "min-docs", + Title: "MIN String Guide", + Description: "Guide for MIN mode string operations", + } + av, err := basestringmin.ItemInput(item) + require.NoError(t, err, "Should marshal MIN string item") + assert.NotEmpty(t, av, "Marshaled item should not be empty") + + assert.Contains(t, av, "id", "Should contain id field") + assert.Contains(t, av, "category", "Should contain category field") + assert.Contains(t, av, "title", "Should contain title field") + assert.Contains(t, av, "description", "Should contain description field") + + assert.IsType(t, &types.AttributeValueMemberS{}, av[basestringmin.ColumnId]) + assert.IsType(t, &types.AttributeValueMemberS{}, av[basestringmin.ColumnCategory]) + assert.IsType(t, &types.AttributeValueMemberS{}, av[basestringmin.ColumnTitle]) + assert.IsType(t, &types.AttributeValueMemberS{}, av[basestringmin.ColumnDescription]) + + _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(basestringmin.TableName), + Item: av, + }) + require.NoError(t, err, "Should store MIN string item in DynamoDB") + key, err := basestringmin.KeyInput(item) + require.NoError(t, err, "Should create key from item") + + getResult, err := client.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(basestringmin.TableName), + Key: key, + }) + require.NoError(t, err, "Should retrieve MIN string item") + assert.NotEmpty(t, getResult.Item, "Retrieved item should not be empty") + + assert.Equal(t, "min-string-001", getResult.Item[basestringmin.ColumnId].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "min-docs", getResult.Item[basestringmin.ColumnCategory].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "MIN String Guide", getResult.Item[basestringmin.ColumnTitle].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "Guide for MIN mode string operations", getResult.Item[basestringmin.ColumnDescription].(*types.AttributeValueMemberS).Value) + + item.Title = "Updated MIN String Guide" + item.Description = "Updated guide for MIN mode string operations" + + updateInput, err := basestringmin.UpdateItemInput(item) + require.NoError(t, err, "Should create update input from item") + + _, err = client.UpdateItem(ctx, updateInput) + require.NoError(t, err, "Should update MIN string item") + + getResult, err = client.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(basestringmin.TableName), + Key: key, + }) + require.NoError(t, err, "Should retrieve updated item") + + assert.Equal(t, "Updated MIN String Guide", getResult.Item[basestringmin.ColumnTitle].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "Updated guide for MIN mode string operations", getResult.Item[basestringmin.ColumnDescription].(*types.AttributeValueMemberS).Value) + + deleteInput, err := basestringmin.DeleteItemInput(item) + require.NoError(t, err, "Should create delete input from item") + + _, err = client.DeleteItem(ctx, deleteInput) + require.NoError(t, err, "Should delete MIN string item") + + getResult, err = client.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(basestringmin.TableName), + Key: key, + }) + require.NoError(t, err, "GetItem should not error for missing item") + assert.Empty(t, getResult.Item, "MIN string item should be deleted") + t.Logf("✅ MIN mode basic CRUD operations work correctly") + }) + + t.Run("min_raw_operations", func(t *testing.T) { + key, err := basestringmin.KeyInputFromRaw("min-raw-001", "min-category") + require.NoError(t, err, "Should create key from raw values") + assert.NotEmpty(t, key, "Raw key should not be empty") + + updates := map[string]any{ + "title": "Raw MIN String Test", + "description": "Testing raw operations in MIN mode", + } + updateInput, err := basestringmin.UpdateItemInputFromRaw("min-raw-001", "min-category", updates) + require.NoError(t, err, "Should create update input from raw values") + assert.NotNil(t, updateInput, "Update input should be created") + t.Logf("✅ MIN mode raw string operations work correctly") + }) + + t.Run("min_string_edge_cases", func(t *testing.T) { + edgeCases := []basestringmin.SchemaItem{ + {Id: "min-edge-1", Category: "empty", Title: "", Description: "Empty title test"}, + {Id: "min-edge-2", Category: "special", Title: "Special: !@#$%^&*()", Description: "Unicode: 🚀✨"}, + {Id: "min-edge-3", Category: "long", Title: "Very " + string(make([]byte, 50)), Description: "Long string test"}, + } + + for _, item := range edgeCases { + av, err := basestringmin.ItemInput(item) + require.NoError(t, err, "Should handle MIN string edge case: %s", item.Id) + assert.NotEmpty(t, av, "Marshaled edge case should not be empty") + } + t.Logf("✅ MIN mode string edge cases handled successfully") + }) +} + +// ==================== String MIN Raw Object Input ==================== + +func testStringMINInputRaw(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Run("raw_vs_object_min_comparison", func(t *testing.T) { + keyFromRaw, err := basestringmin.KeyInputFromRaw("comparison-min-test", "both-methods") + require.NoError(t, err, "Should create key from raw values") + + item := basestringmin.SchemaItem{ + Id: "comparison-min-test", + Category: "both-methods", + } + keyFromObject, err := basestringmin.KeyInput(item) + require.NoError(t, err, "Should create key from object") + + assert.Equal(t, keyFromRaw, keyFromObject, "Raw and object-based keys should be identical") + + t.Logf("✅ Raw and object-based MIN methods produce identical results") + }) + + t.Run("raw_conditional_operations_min", func(t *testing.T) { + conditionExpr := "#version = :v" + conditionNames := map[string]string{"#version": "version"} + conditionValues := map[string]types.AttributeValue{ + ":v": &types.AttributeValueMemberN{Value: "1"}, + } + + deleteInput, err := basestringmin.DeleteItemInputWithCondition( + "conditional-min-test", "min-condition", + conditionExpr, conditionNames, conditionValues, + ) + require.NoError(t, err, "Should create conditional delete with raw method") + assert.NotNil(t, deleteInput.ConditionExpression, "Should have condition expression") + assert.Equal(t, conditionExpr, *deleteInput.ConditionExpression, "Condition should match") + t.Logf("✅ Raw conditional operations work in MIN mode") + }) +} + +// ==================== String MIN QueryBuilder Tests ==================== + +func testStringMINQueryBuilder(t *testing.T, client *dynamodb.Client, ctx context.Context) { + setupStringMINTestData(t, client, ctx) + + t.Run("min_query_universal_methods", func(t *testing.T) { + qb := basestringmin.NewQueryBuilder(). + With("id", basestringmin.EQ, "min-query-test") + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build query using universal .With() method") + assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") + t.Logf("✅ MIN mode universal .With() method works") + }) + + t.Run("min_query_range_conditions", func(t *testing.T) { + qb := basestringmin.NewQueryBuilder(). + With("id", basestringmin.EQ, "min-query-test"). + With("category", basestringmin.BETWEEN, "api", "tutorial") + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build range query using universal operators") + assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") + t.Logf("✅ MIN mode range conditions work with universal operators") + }) + + t.Run("min_query_with_filters", func(t *testing.T) { + qb := basestringmin.NewQueryBuilder(). + With("id", basestringmin.EQ, "min-query-test"). + Filter("title", basestringmin.EQ, "MIN API Documentation") + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build query with universal .Filter() method") + assert.NotNil(t, queryInput.FilterExpression, "Should have filter expression") + t.Logf("✅ MIN mode universal .Filter() method works") + }) + + t.Run("min_query_execution", func(t *testing.T) { + qb := basestringmin.NewQueryBuilder(). + With("id", basestringmin.EQ, "min-query-test") + + items, err := qb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN mode query") + assert.NotEmpty(t, items, "Should return items") + + for _, item := range items { + assert.Equal(t, "min-query-test", item.Id, "All items should have correct hash key") + assert.NotEmpty(t, item.Category, "All items should have category") + assert.IsType(t, "", item.Title, "Title should be string type") + assert.IsType(t, "", item.Description, "Description should be string type") + } + t.Logf("✅ MIN mode query execution returned %d items", len(items)) + }) +} + +// ==================== String MIN ScanBuilder Tests ==================== + +func testStringMINScanBuilder(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Run("min_scan_universal_filter", func(t *testing.T) { + sb := basestringmin.NewScanBuilder(). + Filter("id", basestringmin.EQ, "min-query-test") + + scanInput, err := sb.BuildScan() + require.NoError(t, err, "Should build scan with universal .Filter() method") + assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") + t.Logf("✅ MIN mode scan universal .Filter() method works") + }) + + t.Run("min_scan_multiple_filters", func(t *testing.T) { + sb := basestringmin.NewScanBuilder(). + Filter("id", basestringmin.EQ, "min-query-test"). + Filter("category", basestringmin.EQ, "api"). + Filter("title", basestringmin.GT, "A") + + scanInput, err := sb.BuildScan() + require.NoError(t, err, "Should build scan with multiple universal filters") + assert.NotNil(t, scanInput.FilterExpression, "Should have filter expression") + t.Logf("✅ MIN mode multiple universal filters work") + }) + + t.Run("min_scan_execution", func(t *testing.T) { + sb := basestringmin.NewScanBuilder(). + Filter("id", basestringmin.EQ, "min-query-test"). + Limit(5) + items, err := sb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN mode scan") + + for _, item := range items { + assert.Equal(t, "min-query-test", item.Id, "Items should match filter") + } + t.Logf("✅ MIN mode scan execution returned %d items", len(items)) + }) +} + +// ==================== String MIN Schema Tests ==================== + +func testStringMINSchema(t *testing.T) { + t.Run("min_schema_structure", func(t *testing.T) { + schema := basestringmin.TableSchema + assert.Equal(t, "base-string-min", schema.TableName, "Table name should match MIN schema") + assert.Equal(t, "id", schema.HashKey, "Hash key should be 'id'") + assert.Equal(t, "category", schema.RangeKey, "Range key should be 'category'") + assert.Len(t, schema.SecondaryIndexes, 0, "Should have no secondary indexes") + t.Logf("✅ MIN mode schema structure validated") + }) + + t.Run("min_constants", func(t *testing.T) { + assert.Equal(t, "base-string-min", basestringmin.TableName, "TableName constant should be correct") + assert.Equal(t, "id", basestringmin.ColumnId, "ColumnId should be correct") + assert.Equal(t, "category", basestringmin.ColumnCategory, "ColumnCategory should be correct") + assert.Equal(t, "title", basestringmin.ColumnTitle, "ColumnTitle should be correct") + assert.Equal(t, "description", basestringmin.ColumnDescription, "ColumnDescription should be correct") + t.Logf("✅ MIN mode constants validated") + }) + + t.Run("min_operators_available", func(t *testing.T) { + assert.NotNil(t, basestringmin.EQ, "EQ operator should be available") + assert.NotNil(t, basestringmin.GT, "GT operator should be available") + assert.NotNil(t, basestringmin.LT, "LT operator should be available") + assert.NotNil(t, basestringmin.GTE, "GTE operator should be available") + assert.NotNil(t, basestringmin.LTE, "LTE operator should be available") + assert.NotNil(t, basestringmin.BETWEEN, "BETWEEN operator should be available") + t.Logf("✅ MIN mode universal operators available") + }) + + t.Run("min_string_attributes", func(t *testing.T) { + expectedPrimary := map[string]string{ + "id": "S", + "category": "S", + } + for _, attr := range basestringmin.TableSchema.Attributes { + expectedType, exists := expectedPrimary[attr.Name] + assert.True(t, exists, "Primary attribute %s should be expected", attr.Name) + assert.Equal(t, expectedType, attr.Type, "Attribute %s should be string type", attr.Name) + } + expectedCommon := map[string]string{ + "title": "S", + "description": "S", + } + for _, attr := range basestringmin.TableSchema.CommonAttributes { + expectedType, exists := expectedCommon[attr.Name] + assert.True(t, exists, "Common attribute %s should be expected", attr.Name) + assert.Equal(t, expectedType, attr.Type, "Attribute %s should be string type", attr.Name) + } + t.Logf("✅ MIN mode string attributes validated") + }) + + t.Run("min_no_sugar_methods", func(t *testing.T) { + qb := basestringmin.NewQueryBuilder() + assert.NotNil(t, qb, "QueryBuilder should be available") + + sb := basestringmin.NewScanBuilder() + assert.NotNil(t, sb, "ScanBuilder should be available") + t.Logf("✅ MIN mode builders available (sugar methods should be absent)") + }) +} + +// ==================== Helper Functions ==================== + +func setupStringMINTestData(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Helper() + + testItems := []basestringmin.SchemaItem{ + {Id: "min-query-test", Category: "api", Title: "MIN API Documentation", Description: "REST API guide for MIN mode"}, + {Id: "min-query-test", Category: "sdk", Title: "MIN SDK Reference", Description: "Complete SDK documentation for MIN"}, + {Id: "min-query-test", Category: "tutorial", Title: "MIN Getting Started", Description: "Quick start tutorial for MIN mode"}, + } + for _, item := range testItems { + av, err := basestringmin.ItemInput(item) + require.NoError(t, err, "Should marshal MIN test item") + + _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(basestringmin.TableName), + Item: av, + }) + require.NoError(t, err, "Should store MIN test item") + } + t.Logf("MIN setup complete: inserted %d string test items", len(testItems)) +} diff --git a/tests/localstack/custom_number_test.go b/tests/localstack/custom_number_test.go index 6eeea4e..7e1bcf4 100644 --- a/tests/localstack/custom_number_test.go +++ b/tests/localstack/custom_number_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - customnumber "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/customnumber" + customnumber "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/customnumberall" ) // TestCustomNumber focuses on custom Number subtypes operations and functionality. @@ -24,8 +24,8 @@ import ( // - Custom types in QueryBuilder and ScanBuilder // - ExtractFromDynamoDBStreamEvent with custom types // -// Schema: custom-number.json -// - Table: "custom-number" +// Schema: custom-number__all.json +// - Table: "custom-number-all" // - Hash Key: id (S) // - Range Key: timestamp (N with int64 subtype) // - Common: count (int32), price (float32), views (uint64), score (int16) @@ -61,7 +61,6 @@ func TestCustomNumber(t *testing.T) { func testCustomTypesStruct(t *testing.T) { t.Run("verify_go_types", func(t *testing.T) { - // Создаем item с кастомными типами item := customnumber.SchemaItem{ Id: "test-id", Timestamp: 1640995200, // int64 @@ -71,27 +70,23 @@ func testCustomTypesStruct(t *testing.T) { Score: 85, // int16 } - // Проверяем что компилятор принимает правильные типы - assert.IsType(t, "", item.Id) // string - assert.IsType(t, int64(0), item.Timestamp) // int64 - assert.IsType(t, int32(0), item.Count) // int32 - assert.IsType(t, float32(0), item.Price) // float32 - assert.IsType(t, uint64(0), item.Views) // uint64 - assert.IsType(t, int16(0), item.Score) // int16 + assert.IsType(t, "", item.Id) + assert.IsType(t, int64(0), item.Timestamp) + assert.IsType(t, int32(0), item.Count) + assert.IsType(t, float32(0), item.Price) + assert.IsType(t, uint64(0), item.Views) + assert.IsType(t, int16(0), item.Score) - // Проверяем что значения сохранились правильно assert.Equal(t, "test-id", item.Id) assert.Equal(t, int64(1640995200), item.Timestamp) assert.Equal(t, int32(42), item.Count) assert.Equal(t, float32(19.99), item.Price) assert.Equal(t, uint64(1000000), item.Views) assert.Equal(t, int16(85), item.Score) - t.Logf("✅ Custom types verified: int64, int32, float32, uint64, int16") }) t.Run("type_safety_compilation", func(t *testing.T) { - // Эти присваивания должны компилироваться без ошибок var item customnumber.SchemaItem item.Timestamp = int64(1640995200) @@ -100,13 +95,11 @@ func testCustomTypesStruct(t *testing.T) { item.Views = uint64(1000000) item.Score = int16(85) - // Проверяем что присваивания работают assert.Equal(t, int64(1640995200), item.Timestamp) assert.Equal(t, int32(42), item.Count) assert.Equal(t, float32(19.99), item.Price) assert.Equal(t, uint64(1000000), item.Views) assert.Equal(t, int16(85), item.Score) - t.Logf("✅ Type safety compilation verified") }) } @@ -124,12 +117,10 @@ func testCustomTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx contex Score: 85, } - // Test marshaling with custom types av, err := customnumber.ItemInput(item) require.NoError(t, err, "Should marshal item with custom types") assert.NotEmpty(t, av, "Marshaled item should not be empty") - // Verify all fields are present and properly typed assert.Contains(t, av, "id", "Should contain id field") assert.Contains(t, av, "timestamp", "Should contain timestamp field") assert.Contains(t, av, "count", "Should contain count field") @@ -137,14 +128,12 @@ func testCustomTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx contex assert.Contains(t, av, "views", "Should contain views field") assert.Contains(t, av, "score", "Should contain score field") - // Verify DynamoDB types (all custom numeric types should be marshaled as N) assert.IsType(t, &types.AttributeValueMemberS{}, av[customnumber.ColumnId]) assert.IsType(t, &types.AttributeValueMemberN{}, av[customnumber.ColumnTimestamp]) assert.IsType(t, &types.AttributeValueMemberN{}, av[customnumber.ColumnCount]) assert.IsType(t, &types.AttributeValueMemberN{}, av[customnumber.ColumnPrice]) assert.IsType(t, &types.AttributeValueMemberN{}, av[customnumber.ColumnViews]) assert.IsType(t, &types.AttributeValueMemberN{}, av[customnumber.ColumnScore]) - t.Logf("✅ Custom types marshaled successfully") }) @@ -158,7 +147,6 @@ func testCustomTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx contex Score: 95, } - // Marshal and store av, err := customnumber.ItemInput(originalItem) require.NoError(t, err, "Should marshal original item") @@ -168,7 +156,6 @@ func testCustomTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx contex }) require.NoError(t, err, "Should store item in DynamoDB") - // Retrieve and verify key, err := customnumber.KeyInput(originalItem) require.NoError(t, err, "Should create key from item") @@ -179,14 +166,12 @@ func testCustomTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx contex require.NoError(t, err, "Should retrieve item") assert.NotEmpty(t, getResult.Item, "Retrieved item should not be empty") - // Verify values are preserved with correct precision assert.Equal(t, "roundtrip-test-001", getResult.Item[customnumber.ColumnId].(*types.AttributeValueMemberS).Value) assert.Equal(t, "1640995300", getResult.Item[customnumber.ColumnTimestamp].(*types.AttributeValueMemberN).Value) assert.Equal(t, "123", getResult.Item[customnumber.ColumnCount].(*types.AttributeValueMemberN).Value) assert.Equal(t, "29.95", getResult.Item[customnumber.ColumnPrice].(*types.AttributeValueMemberN).Value) assert.Equal(t, "2000000", getResult.Item[customnumber.ColumnViews].(*types.AttributeValueMemberN).Value) assert.Equal(t, "95", getResult.Item[customnumber.ColumnScore].(*types.AttributeValueMemberN).Value) - t.Logf("✅ Custom types roundtrip successful") }) @@ -194,27 +179,27 @@ func testCustomTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx contex edgeItems := []customnumber.SchemaItem{ { Id: "edge-1", - Timestamp: 0, // int64 zero - Count: 0, // int32 zero - Price: 0.0, // float32 zero - Views: 0, // uint64 zero - Score: 0, // int16 zero + Timestamp: 0, + Count: 0, + Price: 0.0, + Views: 0, + Score: 0, }, { Id: "edge-2", - Timestamp: 9223372036854775807, // int64 max - Count: 2147483647, // int32 max - Price: 3.4028235e+38, // float32 max - Views: 18446744073709551615, // uint64 max - Score: 32767, // int16 max + Timestamp: 9223372036854775807, + Count: 2147483647, + Price: 3.4028235e+38, + Views: 18446744073709551615, + Score: 32767, }, { Id: "edge-3", - Timestamp: -9223372036854775808, // int64 min - Count: -2147483648, // int32 min - Price: -3.4028235e+38, // float32 min - Views: 0, // uint64 min (can't be negative) - Score: -32768, // int16 min + Timestamp: -9223372036854775808, + Count: -2147483648, + Price: -3.4028235e+38, + Views: 0, + Score: -32768, }, } @@ -246,7 +231,6 @@ func testCustomTypesQueryBuilder(t *testing.T, client *dynamodb.Client, ctx cont queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build query with custom int64 parameter") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ Custom types parameters accepted by QueryBuilder") }) @@ -261,7 +245,6 @@ func testCustomTypesQueryBuilder(t *testing.T, client *dynamodb.Client, ctx cont queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build query with custom type filters") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ Custom types filters work in QueryBuilder") }) @@ -279,7 +262,6 @@ func testCustomTypesQueryBuilder(t *testing.T, client *dynamodb.Client, ctx cont assert.IsType(t, uint64(0), item.Views, "Views should be uint64") assert.IsType(t, int16(0), item.Score, "Score should be int16") } - t.Logf("✅ Custom types query execution returned %d items", len(items)) }) } @@ -302,7 +284,6 @@ func testCustomTypesRangeConditions(t *testing.T, client *dynamodb.Client, ctx c assert.GreaterOrEqual(t, item.Timestamp, int64(1640995200), "Timestamp should be >= start") assert.LessOrEqual(t, item.Timestamp, int64(1640995500), "Timestamp should be <= end") } - t.Logf("✅ int64 timestamp range conditions work correctly") }) @@ -313,7 +294,6 @@ func testCustomTypesRangeConditions(t *testing.T, client *dynamodb.Client, ctx c _, err := qb.BuildQuery() require.NoError(t, err, "Should build int32 count between condition") - t.Logf("✅ int32 count range conditions compile correctly") }) @@ -324,7 +304,6 @@ func testCustomTypesRangeConditions(t *testing.T, client *dynamodb.Client, ctx c _, err := qb.BuildQuery() require.NoError(t, err, "Should build uint64 views greater than condition") - t.Logf("✅ uint64 views range conditions compile correctly") }) } @@ -333,9 +312,6 @@ func testCustomTypesRangeConditions(t *testing.T, client *dynamodb.Client, ctx c func testCustomTypesStreamEvent(t *testing.T) { t.Run("extract_custom_types_logic", func(t *testing.T) { - // Проверяем что в сгенерированном коде есть правильная логика для кастомных типов - // Это косвенный тест - мы проверяем что код компилируется и имеет правильную структуру - item := customnumber.SchemaItem{ Id: "stream-test", Timestamp: 1640995600, @@ -345,13 +321,11 @@ func testCustomTypesStreamEvent(t *testing.T) { Score: 90, } - // Если код компилируется и эти присваивания работают, значит типы правильные assert.Equal(t, int64(1640995600), item.Timestamp) assert.Equal(t, int32(75), item.Count) assert.Equal(t, float32(39.99), item.Price) assert.Equal(t, uint64(3000000), item.Views) assert.Equal(t, int16(90), item.Score) - t.Logf("✅ Custom types stream event extraction logic verified") }) } @@ -366,7 +340,6 @@ func setupCustomTypesTestData(t *testing.T, client *dynamodb.Client, ctx context {Id: "query-custom-test", Timestamp: 1640995400, Count: 55, Price: 29.99, Views: 2500000, Score: 90}, {Id: "query-custom-test", Timestamp: 1640995500, Count: 65, Price: 39.99, Views: 3500000, Score: 95}, } - for _, item := range testItems { av, err := customnumber.ItemInput(item) require.NoError(t, err, "Should marshal custom types test item") @@ -377,6 +350,5 @@ func setupCustomTypesTestData(t *testing.T, client *dynamodb.Client, ctx context }) require.NoError(t, err, "Should store custom types test item") } - t.Logf("Setup complete: inserted %d custom types test items", len(testItems)) } diff --git a/tests/localstack/custon_set_number_test.go b/tests/localstack/custom_set_number_test.go similarity index 93% rename from tests/localstack/custon_set_number_test.go rename to tests/localstack/custom_set_number_test.go index c23afd0..79644c1 100644 --- a/tests/localstack/custon_set_number_test.go +++ b/tests/localstack/custom_set_number_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - customsetnumber "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/customsetnumber" + customsetnumber "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/customsetnumberall" ) // TestCustomSetNumber focuses on custom Number Set subtypes operations and functionality. @@ -24,8 +24,8 @@ import ( // - Custom types in QueryBuilder and ScanBuilder with Contains filters // - Set operations with different numeric types // -// Schema: custom-set-number.json -// - Table: "custom-set-number" +// Schema: custom-set-number__all.json +// - Table: "custom-set-number-all" // - Hash Key: id (S) // - Range Key: group_id (S) // - Common: int32_scores (NS with int32 subtype), int64_timestamps (NS with int64 subtype), @@ -88,7 +88,6 @@ func testCustomSetTypesStruct(t *testing.T) { assert.Equal(t, []float32{19.99, 25.50, 30.75}, item.Float32Rates) assert.Equal(t, []uint64{1000000, 2000000, 3000000}, item.Uint64Counters) assert.Equal(t, []int16{100, 200, 300}, item.Int16Values) - t.Logf("✅ Custom set types verified: []int32, []int64, []float32, []uint64, []int16") }) @@ -106,7 +105,6 @@ func testCustomSetTypesStruct(t *testing.T) { assert.Equal(t, []float32{19.99, 29.99}, item.Float32Rates) assert.Equal(t, []uint64{1000000}, item.Uint64Counters) assert.Equal(t, []int16{85, 95}, item.Int16Values) - t.Logf("✅ Type safety compilation verified") }) } @@ -124,13 +122,10 @@ func testCustomSetTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx con Uint64Counters: []uint64{1000000, 2000000}, Int16Values: []int16{100, 200, 300, 400}, } - - // Test marshaling with custom set types av, err := customsetnumber.ItemInput(item) require.NoError(t, err, "Should marshal item with custom set types") assert.NotEmpty(t, av, "Marshaled item should not be empty") - // Verify all fields are present and properly typed assert.Contains(t, av, "id", "Should contain id field") assert.Contains(t, av, "group_id", "Should contain group_id field") assert.Contains(t, av, "int32_scores", "Should contain int32_scores field") @@ -139,7 +134,6 @@ func testCustomSetTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx con assert.Contains(t, av, "uint64_counters", "Should contain uint64_counters field") assert.Contains(t, av, "int16_values", "Should contain int16_values field") - // Verify DynamoDB types (all custom numeric sets should be marshaled as NS) assert.IsType(t, &types.AttributeValueMemberS{}, av[customsetnumber.ColumnId]) assert.IsType(t, &types.AttributeValueMemberS{}, av[customsetnumber.ColumnGroupId]) assert.IsType(t, &types.AttributeValueMemberNS{}, av[customsetnumber.ColumnInt32Scores]) @@ -147,7 +141,6 @@ func testCustomSetTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx con assert.IsType(t, &types.AttributeValueMemberNS{}, av[customsetnumber.ColumnFloat32Rates]) assert.IsType(t, &types.AttributeValueMemberNS{}, av[customsetnumber.ColumnUint64Counters]) assert.IsType(t, &types.AttributeValueMemberNS{}, av[customsetnumber.ColumnInt16Values]) - t.Logf("✅ Custom set types marshaled successfully") }) @@ -162,7 +155,6 @@ func testCustomSetTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx con Int16Values: []int16{95, 85, 75}, } - // Marshal and store av, err := customsetnumber.ItemInput(originalItem) require.NoError(t, err, "Should marshal original item") @@ -172,7 +164,6 @@ func testCustomSetTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx con }) require.NoError(t, err, "Should store item in DynamoDB") - // Retrieve and verify key, err := customsetnumber.KeyInput(originalItem) require.NoError(t, err, "Should create key from item") @@ -183,11 +174,9 @@ func testCustomSetTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx con require.NoError(t, err, "Should retrieve item") assert.NotEmpty(t, getResult.Item, "Retrieved item should not be empty") - // Verify values are preserved as number sets assert.Equal(t, "roundtrip-test-001", getResult.Item[customsetnumber.ColumnId].(*types.AttributeValueMemberS).Value) assert.Equal(t, "roundtrip-group", getResult.Item[customsetnumber.ColumnGroupId].(*types.AttributeValueMemberS).Value) - // Check that all sets contain expected string representations int32Set := getResult.Item[customsetnumber.ColumnInt32Scores].(*types.AttributeValueMemberNS) assert.Contains(t, int32Set.Value, "123") assert.Contains(t, int32Set.Value, "456") @@ -208,7 +197,6 @@ func testCustomSetTypesMarshaling(t *testing.T, client *dynamodb.Client, ctx con int16Set := getResult.Item[customsetnumber.ColumnInt16Values].(*types.AttributeValueMemberNS) assert.Contains(t, int16Set.Value, "95") assert.Contains(t, int16Set.Value, "85") - t.Logf("✅ Custom set types roundtrip successful") }) } @@ -229,7 +217,6 @@ func testCustomSetTypesQueryBuilder(t *testing.T, client *dynamodb.Client, ctx c queryInput, err := qb.BuildQuery() require.NoError(t, err, "Should build query with custom set type contains filters") assert.NotNil(t, queryInput.KeyConditionExpression, "Should have key condition") - t.Logf("✅ Custom set types contains filters work in QueryBuilder") }) @@ -247,7 +234,6 @@ func testCustomSetTypesQueryBuilder(t *testing.T, client *dynamodb.Client, ctx c assert.IsType(t, []uint64{}, item.Uint64Counters, "Uint64Counters should be []uint64") assert.IsType(t, []int16{}, item.Int16Values, "Int16Values should be []int16") } - t.Logf("✅ Custom set types query execution returned %d items", len(items)) }) } @@ -255,7 +241,6 @@ func testCustomSetTypesQueryBuilder(t *testing.T, client *dynamodb.Client, ctx c // ==================== Custom Set Operations Tests ==================== func testCustomSetOperations(t *testing.T, client *dynamodb.Client, ctx context.Context) { - // Setup item for set operations testing testItem := customsetnumber.SchemaItem{ Id: "set-ops-test", GroupId: "operations", @@ -294,7 +279,6 @@ func testCustomSetOperations(t *testing.T, client *dynamodb.Client, ctx context. assert.Contains(t, int32Set.Value, "20", "Should still contain initial value") assert.Contains(t, int32Set.Value, "30", "Should contain added value") assert.Contains(t, int32Set.Value, "40", "Should contain added value") - t.Logf("✅ Added to int32 set successfully") }) @@ -317,7 +301,6 @@ func testCustomSetOperations(t *testing.T, client *dynamodb.Client, ctx context. assert.Contains(t, float32Set.Value, "2.5", "Should still contain initial value") assert.Contains(t, float32Set.Value, "3.5", "Should contain added value") assert.Contains(t, float32Set.Value, "4.5", "Should contain added value") - t.Logf("✅ Added to float32 set successfully") }) @@ -338,7 +321,6 @@ func testCustomSetOperations(t *testing.T, client *dynamodb.Client, ctx context. uint64Set := getResult.Item[customsetnumber.ColumnUint64Counters].(*types.AttributeValueMemberNS) assert.Contains(t, uint64Set.Value, "200", "Should still contain remaining value") assert.NotContains(t, uint64Set.Value, "100", "Should not contain removed value") - t.Logf("✅ Removed from uint64 set successfully") }) } @@ -360,23 +342,22 @@ func testCustomSetEdgeValues(t *testing.T, client *dynamodb.Client, ctx context. { Id: "edge-2", GroupId: "max-values", - Int32Scores: []int32{2147483647}, // int32 max - Int64Timestamps: []int64{9223372036854775807}, // int64 max - Float32Rates: []float32{3.4028235e+38}, // float32 max - Uint64Counters: []uint64{18446744073709551615}, // uint64 max - Int16Values: []int16{32767}, // int16 max + Int32Scores: []int32{2147483647}, + Int64Timestamps: []int64{9223372036854775807}, + Float32Rates: []float32{3.4028235e+38}, + Uint64Counters: []uint64{18446744073709551615}, + Int16Values: []int16{32767}, }, { Id: "edge-3", GroupId: "min-values", - Int32Scores: []int32{-2147483648}, // int32 min - Int64Timestamps: []int64{-9223372036854775808}, // int64 min - Float32Rates: []float32{-3.4028235e+38}, // float32 min - Uint64Counters: []uint64{0}, // uint64 min (can't be negative) - Int16Values: []int16{-32768}, // int16 min + Int32Scores: []int32{-2147483648}, + Int64Timestamps: []int64{-9223372036854775808}, + Float32Rates: []float32{-3.4028235e+38}, + Uint64Counters: []uint64{0}, + Int16Values: []int16{-32768}, }, } - for _, item := range edgeItems { av, err := customsetnumber.ItemInput(item) require.NoError(t, err, "Should handle edge values for item: %s", item.Id) @@ -387,7 +368,6 @@ func testCustomSetEdgeValues(t *testing.T, client *dynamodb.Client, ctx context. }) require.NoError(t, err, "Should store edge value item: %s", item.Id) } - t.Logf("✅ Custom set types edge values handled successfully") }) } @@ -417,7 +397,6 @@ func setupCustomSetTypesTestData(t *testing.T, client *dynamodb.Client, ctx cont Int16Values: []int16{400, 500}, }, } - for _, item := range testItems { av, err := customsetnumber.ItemInput(item) require.NoError(t, err, "Should marshal custom set types test item") @@ -428,6 +407,5 @@ func setupCustomSetTypesTestData(t *testing.T, client *dynamodb.Client, ctx cont }) require.NoError(t, err, "Should store custom set types test item") } - t.Logf("Setup complete: inserted %d custom set types test items", len(testItems)) } diff --git a/tests/localstack/user_post_complete_test.go b/tests/localstack/user_post_complete_all_test.go similarity index 98% rename from tests/localstack/user_post_complete_test.go rename to tests/localstack/user_post_complete_all_test.go index a368308..cf95a91 100644 --- a/tests/localstack/user_post_complete_test.go +++ b/tests/localstack/user_post_complete_all_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - userpostscomplete "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/userpostscomplete" + userpostscomplete "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/userpostscompleteall" ) // TestUserPostsComplete focuses on GSI/LSI mixed operations and functionality. @@ -24,8 +24,8 @@ import ( // - Complex projection types (ALL, KEYS_ONLY, INCLUDE) // - Multiple index types on same table // -// Schema: user-posts-complete.json -// - Table: "user-posts-complete" +// Schema: user-posts-complete__all.json +// - Table: "user-posts-complete-all" // - Hash Key: user_id (S) // - Range Key: created_at (S) // - LSI: lsi_by_post_type, lsi_by_status, lsi_by_priority @@ -289,14 +289,14 @@ func testUserPostsSchema(t *testing.T) { t.Run("table_schema_structure", func(t *testing.T) { schema := userpostscomplete.TableSchema - assert.Equal(t, "user-posts-complete", schema.TableName) + assert.Equal(t, "user-posts-complete-all", schema.TableName) assert.Equal(t, "user_id", schema.HashKey) assert.Equal(t, "created_at", schema.RangeKey) assert.Len(t, schema.SecondaryIndexes, 6, "Should have 6 secondary indexes (3 LSI + 3 GSI)") }) t.Run("constants_validation", func(t *testing.T) { - assert.Equal(t, "user-posts-complete", userpostscomplete.TableName) + assert.Equal(t, "user-posts-complete-all", userpostscomplete.TableName) assert.Equal(t, "user_id", userpostscomplete.ColumnUserId) assert.Equal(t, "created_at", userpostscomplete.ColumnCreatedAt) assert.Equal(t, "post_type", userpostscomplete.ColumnPostType) diff --git a/tests/localstack/user_post_complete_min_test.go b/tests/localstack/user_post_complete_min_test.go new file mode 100644 index 0000000..27ac542 --- /dev/null +++ b/tests/localstack/user_post_complete_min_test.go @@ -0,0 +1,475 @@ +package localstack + +import ( + "context" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + userpostscompletemin "github.com/Mad-Pixels/go-dyno/tests/localstack/generated/userpostscompletemin" +) + +// TestUserPostsCompleteMIN focuses on GSI/LSI mixed operations with minimal generated code. +// This test validates the new architecture with both GSI and LSI indexes using universal methods only. +// +// Test Coverage: +// - Mixed GSI/LSI CRUD operations using universal methods +// - Index selection and query optimization in MIN mode +// - GSI vs LSI query performance with basic methods +// - Complex projection types using .With() and .Filter() +// - Multiple index types on same table in MIN mode +// +// Schema: user-posts-complete__min.json +// - Table: "user-posts-complete-min" +// - Hash Key: user_id (S) +// - Range Key: created_at (S) +// - LSI: lsi_by_post_type, lsi_by_status, lsi_by_priority +// - GSI: gsi_by_category, gsi_by_title, gsi_by_status_priority +// +// Note: MIN mode only includes universal methods like .With() and .Filter() +// No convenience methods like .WithEQ(), .FilterEQ(), .WithBetween() etc. +func TestUserPostsCompleteMIN(t *testing.T) { + client := ConnectToLocalStack(t, DefaultLocalStackConfig()) + ctx, cancel := TestContext(3 * time.Minute) + defer cancel() + + t.Logf("Testing MIN mode GSI/LSI operations on: %s", userpostscompletemin.TableName) + + t.Run("UserPostsMIN_Input", func(t *testing.T) { + testUserPostsMINInput(t, client, ctx) + }) + + t.Run("UserPostsMIN_QueryBuilder_LSI", func(t *testing.T) { + testUserPostsMINQueryBuilderLSI(t, client, ctx) + }) + + t.Run("UserPostsMIN_QueryBuilder_GSI", func(t *testing.T) { + testUserPostsMINQueryBuilderGSI(t, client, ctx) + }) + + t.Run("UserPostsMIN_Schema", func(t *testing.T) { + t.Parallel() + testUserPostsMINSchema(t) + }) +} + +// ==================== User Posts MIN Input ==================== + +func testUserPostsMINInput(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Run("min_complete_crud", func(t *testing.T) { + item := userpostscompletemin.SchemaItem{ + UserId: "min-user123", + CreatedAt: "2024-01-15T10:30:00Z", + PostType: "blog", + Status: "published", + Priority: 85, + Category: "technology", + Title: "MIN Introduction to DynamoDB", + Content: "This is a comprehensive guide to DynamoDB in MIN mode", + Tags: []string{"aws", "database", "nosql"}, + ViewCount: 1500, + UpdatedAt: "2024-01-16T09:15:00Z", + } + + av, err := userpostscompletemin.ItemInput(item) + require.NoError(t, err, "Should marshal MIN complete item") + assert.NotEmpty(t, av, "Marshaled item should not be empty") + + assert.Contains(t, av, "user_id", "Should contain user_id field") + assert.Contains(t, av, "created_at", "Should contain created_at field") + assert.Contains(t, av, "post_type", "Should contain post_type field") + assert.Contains(t, av, "status", "Should contain status field") + assert.Contains(t, av, "priority", "Should contain priority field") + assert.Contains(t, av, "category", "Should contain category field") + assert.Contains(t, av, "title", "Should contain title field") + assert.Contains(t, av, "content", "Should contain content field") + assert.Contains(t, av, "tags", "Should contain tags field") + assert.Contains(t, av, "view_count", "Should contain view_count field") + + assert.IsType(t, &types.AttributeValueMemberS{}, av[userpostscompletemin.ColumnUserId]) + assert.IsType(t, &types.AttributeValueMemberS{}, av[userpostscompletemin.ColumnCreatedAt]) + assert.IsType(t, &types.AttributeValueMemberS{}, av[userpostscompletemin.ColumnPostType]) + assert.IsType(t, &types.AttributeValueMemberS{}, av[userpostscompletemin.ColumnStatus]) + assert.IsType(t, &types.AttributeValueMemberN{}, av[userpostscompletemin.ColumnPriority]) + assert.IsType(t, &types.AttributeValueMemberS{}, av[userpostscompletemin.ColumnCategory]) + assert.IsType(t, &types.AttributeValueMemberS{}, av[userpostscompletemin.ColumnTitle]) + assert.IsType(t, &types.AttributeValueMemberS{}, av[userpostscompletemin.ColumnContent]) + assert.IsType(t, &types.AttributeValueMemberSS{}, av[userpostscompletemin.ColumnTags]) + assert.IsType(t, &types.AttributeValueMemberN{}, av[userpostscompletemin.ColumnViewCount]) + + _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(userpostscompletemin.TableName), + Item: av, + }) + require.NoError(t, err, "Should store MIN complete item in DynamoDB") + + key, err := userpostscompletemin.KeyInput(item) + require.NoError(t, err, "Should create key from item") + + getResult, err := client.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(userpostscompletemin.TableName), + Key: key, + }) + require.NoError(t, err, "Should retrieve MIN complete item") + assert.NotEmpty(t, getResult.Item, "Retrieved item should not be empty") + + assert.Equal(t, "min-user123", getResult.Item[userpostscompletemin.ColumnUserId].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "2024-01-15T10:30:00Z", getResult.Item[userpostscompletemin.ColumnCreatedAt].(*types.AttributeValueMemberS).Value) + + assert.Equal(t, "blog", getResult.Item[userpostscompletemin.ColumnPostType].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "published", getResult.Item[userpostscompletemin.ColumnStatus].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "85", getResult.Item[userpostscompletemin.ColumnPriority].(*types.AttributeValueMemberN).Value) + assert.Equal(t, "technology", getResult.Item[userpostscompletemin.ColumnCategory].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "MIN Introduction to DynamoDB", getResult.Item[userpostscompletemin.ColumnTitle].(*types.AttributeValueMemberS).Value) + + tagsSet := getResult.Item[userpostscompletemin.ColumnTags].(*types.AttributeValueMemberSS) + assert.Contains(t, tagsSet.Value, "aws") + assert.Contains(t, tagsSet.Value, "database") + assert.Contains(t, tagsSet.Value, "nosql") + + item.Title = "Updated MIN Introduction to DynamoDB" + item.ViewCount = 2000 + + updateInput, err := userpostscompletemin.UpdateItemInput(item) + require.NoError(t, err, "Should create update input from item") + + _, err = client.UpdateItem(ctx, updateInput) + require.NoError(t, err, "Should update MIN complete item") + + getResult, err = client.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(userpostscompletemin.TableName), + Key: key, + }) + require.NoError(t, err, "Should retrieve updated item") + + assert.Equal(t, "Updated MIN Introduction to DynamoDB", getResult.Item[userpostscompletemin.ColumnTitle].(*types.AttributeValueMemberS).Value) + assert.Equal(t, "2000", getResult.Item[userpostscompletemin.ColumnViewCount].(*types.AttributeValueMemberN).Value) + + deleteInput, err := userpostscompletemin.DeleteItemInput(item) + require.NoError(t, err, "Should create delete input from item") + + _, err = client.DeleteItem(ctx, deleteInput) + require.NoError(t, err, "Should delete MIN complete item") + + getResult, err = client.GetItem(ctx, &dynamodb.GetItemInput{ + TableName: aws.String(userpostscompletemin.TableName), + Key: key, + }) + require.NoError(t, err, "GetItem should not error for missing item") + assert.Empty(t, getResult.Item, "MIN complete item should be deleted") + + t.Logf("✅ MIN mode complete CRUD operations work correctly") + }) + + t.Run("min_raw_operations", func(t *testing.T) { + key, err := userpostscompletemin.KeyInputFromRaw("min-raw-user", "2024-01-01T12:00:00Z") + require.NoError(t, err, "Should create key from raw values") + assert.NotEmpty(t, key, "Raw key should not be empty") + + updates := map[string]any{ + "title": "MIN Raw Update Test", + "view_count": 500, + "status": "published", + } + + updateInput, err := userpostscompletemin.UpdateItemInputFromRaw("min-raw-user", "2024-01-01T12:00:00Z", updates) + require.NoError(t, err, "Should create update input from raw values") + assert.NotNil(t, updateInput, "Update input should be created") + + t.Logf("✅ MIN mode raw operations work correctly") + }) +} + +// ==================== LSI QueryBuilder Tests ==================== + +func testUserPostsMINQueryBuilderLSI(t *testing.T, client *dynamodb.Client, ctx context.Context) { + setupUserPostsMINTestData(t, client, ctx) + + t.Run("min_lsi_by_post_type", func(t *testing.T) { + qb := userpostscompletemin.NewQueryBuilder(). + With("user_id", userpostscompletemin.EQ, "min-query-test-user"). + With("post_type", userpostscompletemin.EQ, "tutorial") + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build MIN LSI query by post_type") + + t.Logf("MIN IndexName: %v", queryInput.IndexName) + t.Logf("MIN KeyConditionExpression: %v", queryInput.KeyConditionExpression) + + items, err := qb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN LSI query") + + t.Logf("MIN Returned %d items", len(items)) + for i, item := range items { + t.Logf("MIN Item %d: user_id=%s, post_type=%s", i, item.UserId, item.PostType) + assert.Equal(t, "min-query-test-user", item.UserId) + assert.Equal(t, "tutorial", item.PostType) + } + t.Logf("✅ MIN mode LSI by post_type query works") + }) + + t.Run("min_lsi_by_status", func(t *testing.T) { + qb := userpostscompletemin.NewQueryBuilder(). + With("user_id", userpostscompletemin.EQ, "min-query-test-user"). + With("status", userpostscompletemin.EQ, "published") + + _, err := qb.BuildQuery() + require.NoError(t, err, "Should build MIN LSI query by status") + + items, err := qb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN LSI status query") + + for _, item := range items { + assert.Equal(t, "min-query-test-user", item.UserId) + assert.Equal(t, "published", item.Status) + } + t.Logf("✅ MIN mode LSI by status query returned %d items", len(items)) + }) + + t.Run("min_lsi_by_priority_with_range", func(t *testing.T) { + qb := userpostscompletemin.NewQueryBuilder(). + With("user_id", userpostscompletemin.EQ, "min-query-test-user"). + With("priority", userpostscompletemin.BETWEEN, 70, 90) + + _, err := qb.BuildQuery() + require.NoError(t, err, "Should build MIN LSI query with priority range") + + items, err := qb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN LSI priority range query") + + for _, item := range items { + assert.Equal(t, "min-query-test-user", item.UserId) + assert.GreaterOrEqual(t, item.Priority, 70) + assert.LessOrEqual(t, item.Priority, 90) + } + t.Logf("✅ MIN mode LSI priority range query returned %d items", len(items)) + }) +} + +// ==================== GSI QueryBuilder Tests ==================== + +func testUserPostsMINQueryBuilderGSI(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Run("min_gsi_by_category", func(t *testing.T) { + qb := userpostscompletemin.NewQueryBuilder(). + With("category", userpostscompletemin.EQ, "technology") + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build MIN GSI query by category") + + if queryInput.IndexName != nil { + assert.Equal(t, "gsi_by_category", *queryInput.IndexName, "Should use GSI index") + } + items, err := qb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN GSI category query") + + for _, item := range items { + assert.Equal(t, "technology", item.Category, "All items should match category") + } + t.Logf("✅ MIN mode GSI by category query returned %d items", len(items)) + }) + + t.Run("min_gsi_by_title", func(t *testing.T) { + qb := userpostscompletemin.NewQueryBuilder(). + With("title", userpostscompletemin.EQ, "MIN Advanced DynamoDB") + + _, err := qb.BuildQuery() + require.NoError(t, err, "Should build MIN GSI query by title") + + items, err := qb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN GSI title query") + + for _, item := range items { + assert.Equal(t, "MIN Advanced DynamoDB", item.Title) + } + t.Logf("✅ MIN mode GSI by title query returned %d items", len(items)) + }) + + t.Run("min_gsi_status_priority_compound", func(t *testing.T) { + qb := userpostscompletemin.NewQueryBuilder(). + With("status", userpostscompletemin.EQ, "published"). + With("priority", userpostscompletemin.GT, 80) + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build MIN compound GSI query") + + items, err := qb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN compound GSI query") + + t.Logf("MIN KeyCondition: %s", aws.ToString(queryInput.KeyConditionExpression)) + + for _, item := range items { + assert.Equal(t, "published", item.Status) + assert.Greater(t, item.Priority, 80) + } + + t.Logf("✅ MIN mode GSI compound query returned %d items", len(items)) + }) + + t.Run("min_gsi_with_filters", func(t *testing.T) { + qb := userpostscompletemin.NewQueryBuilder(). + With("category", userpostscompletemin.EQ, "technology"). + Filter("view_count", userpostscompletemin.GT, 1000) + + queryInput, err := qb.BuildQuery() + require.NoError(t, err, "Should build MIN GSI query with filters") + assert.NotNil(t, queryInput.FilterExpression, "Should have filter expression") + + items, err := qb.Execute(ctx, client) + require.NoError(t, err, "Should execute MIN GSI query with filters") + + for _, item := range items { + assert.Equal(t, "technology", item.Category) + assert.Greater(t, item.ViewCount, 1000) + } + t.Logf("✅ MIN mode GSI with filters returned %d items", len(items)) + }) +} + +// ==================== Schema Tests ==================== + +func testUserPostsMINSchema(t *testing.T) { + t.Run("min_table_schema_structure", func(t *testing.T) { + schema := userpostscompletemin.TableSchema + + assert.Equal(t, "user-posts-complete-min", schema.TableName, "Table name should match MIN schema") + assert.Equal(t, "user_id", schema.HashKey, "Hash key should be 'user_id'") + assert.Equal(t, "created_at", schema.RangeKey, "Range key should be 'created_at'") + assert.Len(t, schema.SecondaryIndexes, 6, "Should have 6 secondary indexes (3 LSI + 3 GSI)") + t.Logf("✅ MIN mode schema structure validated") + }) + + t.Run("min_constants_validation", func(t *testing.T) { + assert.Equal(t, "user-posts-complete-min", userpostscompletemin.TableName, "TableName should be MIN") + assert.Equal(t, "user_id", userpostscompletemin.ColumnUserId, "ColumnUserId should be correct") + assert.Equal(t, "created_at", userpostscompletemin.ColumnCreatedAt, "ColumnCreatedAt should be correct") + assert.Equal(t, "post_type", userpostscompletemin.ColumnPostType, "ColumnPostType should be correct") + assert.Equal(t, "status", userpostscompletemin.ColumnStatus, "ColumnStatus should be correct") + assert.Equal(t, "priority", userpostscompletemin.ColumnPriority, "ColumnPriority should be correct") + assert.Equal(t, "category", userpostscompletemin.ColumnCategory, "ColumnCategory should be correct") + assert.Equal(t, "title", userpostscompletemin.ColumnTitle, "ColumnTitle should be correct") + assert.Equal(t, "content", userpostscompletemin.ColumnContent, "ColumnContent should be correct") + assert.Equal(t, "tags", userpostscompletemin.ColumnTags, "ColumnTags should be correct") + assert.Equal(t, "view_count", userpostscompletemin.ColumnViewCount, "ColumnViewCount should be correct") + assert.Equal(t, "updated_at", userpostscompletemin.ColumnUpdatedAt, "ColumnUpdatedAt should be correct") + t.Logf("✅ MIN mode constants validated") + }) + + t.Run("min_operators_available", func(t *testing.T) { + assert.NotNil(t, userpostscompletemin.EQ, "EQ operator should be available") + assert.NotNil(t, userpostscompletemin.GT, "GT operator should be available") + assert.NotNil(t, userpostscompletemin.LT, "LT operator should be available") + assert.NotNil(t, userpostscompletemin.GTE, "GTE operator should be available") + assert.NotNil(t, userpostscompletemin.LTE, "LTE operator should be available") + assert.NotNil(t, userpostscompletemin.BETWEEN, "BETWEEN operator should be available") + t.Logf("✅ MIN mode universal operators available") + }) + + t.Run("min_attribute_names", func(t *testing.T) { + attrs := userpostscompletemin.AttributeNames + expectedAttrs := []string{ + "user_id", "created_at", "post_type", "status", "priority", + "category", "title", "content", "tags", "view_count", "updated_at", + } + + assert.Len(t, attrs, len(expectedAttrs), "Should have correct number of attributes") + for _, expected := range expectedAttrs { + assert.Contains(t, attrs, expected, "AttributeNames should contain %s", expected) + } + t.Logf("✅ MIN mode AttributeNames validated") + }) + + t.Run("min_secondary_indexes", func(t *testing.T) { + schema := userpostscompletemin.TableSchema + + lsiIndexes := []string{"lsi_by_post_type", "lsi_by_status", "lsi_by_priority"} + gsiIndexes := []string{"gsi_by_category", "gsi_by_title", "gsi_by_status_priority"} + + indexNames := make([]string, 0, len(schema.SecondaryIndexes)) + for _, idx := range schema.SecondaryIndexes { + indexNames = append(indexNames, idx.Name) + } + + for _, expectedLSI := range lsiIndexes { + assert.Contains(t, indexNames, expectedLSI, "Should contain LSI: %s", expectedLSI) + } + + for _, expectedGSI := range gsiIndexes { + assert.Contains(t, indexNames, expectedGSI, "Should contain GSI: %s", expectedGSI) + } + t.Logf("✅ MIN mode secondary indexes validated") + }) + + t.Run("min_no_sugar_methods", func(t *testing.T) { + qb := userpostscompletemin.NewQueryBuilder() + assert.NotNil(t, qb, "QueryBuilder should be available") + + sb := userpostscompletemin.NewScanBuilder() + assert.NotNil(t, sb, "ScanBuilder should be available") + t.Logf("✅ MIN mode builders available (sugar methods should be absent)") + }) +} + +// ==================== Helper Functions ==================== + +func setupUserPostsMINTestData(t *testing.T, client *dynamodb.Client, ctx context.Context) { + t.Helper() + + testItems := []userpostscompletemin.SchemaItem{ + { + UserId: "min-query-test-user", + CreatedAt: "2024-01-01T10:00:00Z", + PostType: "tutorial", + Status: "published", + Priority: 85, + Category: "technology", + Title: "MIN DynamoDB Basics", + Content: "Introduction to DynamoDB concepts in MIN mode", + Tags: []string{"aws", "database"}, + ViewCount: 1200, + UpdatedAt: "2024-01-01T11:00:00Z", + }, + { + UserId: "min-query-test-user", + CreatedAt: "2024-01-02T14:30:00Z", + PostType: "tutorial", + Status: "published", + Priority: 88, + Category: "technology", + Title: "MIN Advanced DynamoDB", + Content: "Advanced DynamoDB patterns in MIN mode", + Tags: []string{"aws", "database", "advanced"}, + ViewCount: 800, + UpdatedAt: "2024-01-02T15:30:00Z", + }, + { + UserId: "min-query-test-user", + CreatedAt: "2024-01-03T09:15:00Z", + PostType: "blog", + Status: "draft", + Priority: 75, + Category: "programming", + Title: "MIN Best Practices", + Content: "Programming best practices in MIN mode", + Tags: []string{"programming", "tips"}, + ViewCount: 450, + UpdatedAt: "2024-01-03T10:15:00Z", + }, + } + + for _, item := range testItems { + av, err := userpostscompletemin.ItemInput(item) + require.NoError(t, err, "Should marshal MIN test item") + + _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(userpostscompletemin.TableName), + Item: av, + }) + require.NoError(t, err, "Should store MIN test item") + } + t.Logf("MIN setup complete: inserted %d test items", len(testItems)) +} diff --git a/tests/validation/schema_validation_test.go b/tests/validation/schema_validation_test.go index 5b186d9..1e8d73f 100644 --- a/tests/validation/schema_validation_test.go +++ b/tests/validation/schema_validation_test.go @@ -63,8 +63,26 @@ func TestSchemaValidation(t *testing.T) { description string }{ { - name: "valid_schema_should_pass", - schemaFile: "base-string.json", + name: "valid_schema_should_pass_base-string-all", + schemaFile: "base-string__all.json", + expectError: false, + description: "Valid schema should load without errors", + }, + { + name: "valid_schema_should_pass_base-boolean-all", + schemaFile: "base-boolean__all.json", + expectError: false, + description: "Valid schema should load without errors", + }, + { + name: "valid_schema_should_pass_user-posts-compile-all", + schemaFile: "user-posts-complete__all.json", + expectError: false, + description: "Valid schema should load without errors", + }, + { + name: "valid_schema_should_pass_user-posts-compile-min", + schemaFile: "user-posts-complete__min.json", expectError: false, description: "Valid schema should load without errors", },