Skip to content

Commit dcf0a40

Browse files
committed
fix tests and params
1 parent aa2cf0f commit dcf0a40

File tree

6 files changed

+171
-264
lines changed

6 files changed

+171
-264
lines changed

docs/yaml-format.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ chains:
6060
| `name` | `task_name` | TEXT | `null` | Task description |
6161
| `kind` | `kind` | ENUM | `'SQL'` | Command type (SQL/PROGRAM/BUILTIN) |
6262
| `command` | `command` | TEXT | **required** | Command to execute |
63-
| `parameters` | via `timetable.parameter` | Array of JSONB | `null` | Array of parameter values, each causing separate task execution |
63+
| `parameters` | via `timetable.parameter` | Array of any | `null` | Array of parameter values stored as individual JSONB rows with order_id |
6464
| `run_as` | `run_as` | TEXT | `null` | Role for SET ROLE |
6565
| `connect_string` | `database_connection` | TEXT | `null` | Connection string |
6666
| `ignore_error` | `ignore_error` | BOOLEAN | `false` | Continue on error |
@@ -131,5 +131,5 @@ chains:
131131
2. **Unique Names**: Chain names must be unique across the database
132132
3. **Valid Cron**: Schedule must be valid cron format (5 fields)
133133
4. **Valid Kind**: Task kind must be one of: SQL, PROGRAM, BUILTIN
134-
5. **Parameter Types**: Parameters must be strings or numbers (converted to JSONB array)
134+
5. **Parameter Types**: Parameters can be any JSON-compatible type (strings, numbers, booleans, arrays, objects) and are stored as individual JSONB values
135135
6. **Timeout Values**: Must be non-negative integers (milliseconds)

docs/yaml-usage-guide.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ Each task can have multiple parameter entries, with each entry causing a separat
8787
command: "Log"
8888
parameters:
8989
- "WARNING: Simple message"
90-
- {"level": "WARNING", "details": "Object message"}
90+
- level: "WARNING"
91+
details: "Object message"
9192

9293
# BUILTIN: SendMail task (complex object)
9394
- name: "mail-task"

internal/pgengine/bootstrap.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -247,21 +247,7 @@ func (pge *PgEngine) ExecuteFileScript(ctx context.Context, cmdOpts config.CmdOp
247247
return pge.ExecuteCustomScripts(ctx, filePath)
248248

249249
default:
250-
// Try to detect content type for files without extension
251-
content, err := os.ReadFile(filePath)
252-
if err != nil {
253-
pge.l.WithError(err).Error("cannot read file")
254-
return err
255-
}
256-
257-
// Check if it looks like YAML (starts with "chains:" or contains YAML markers)
258-
contentStr := strings.TrimSpace(string(content))
259-
if strings.HasPrefix(contentStr, "chains:") {
260-
pge.l.WithField("file", filePath).Info("Detected YAML content, processing as YAML")
261-
return pge.LoadYamlChains(ctx, filePath, false)
262-
}
263-
pge.l.WithField("file", filePath).Info("Processing as SQL script")
264-
return pge.ExecuteCustomScripts(ctx, filePath)
250+
return errors.New("unsupported file extension: " + fileExt)
265251
}
266252
}
267253

internal/pgengine/bootstrap_test.go

Lines changed: 21 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,14 @@ func TestExecuteFileScript(t *testing.T) {
154154
// Create temporary directory for test files
155155
tmpDir := t.TempDir()
156156

157+
anyArgs := func(i int) []any {
158+
args := make([]any, i)
159+
for j := range i {
160+
args[j] = pgxmock.AnyArg()
161+
}
162+
return args
163+
}
164+
157165
t.Run("SQL file execution", func(t *testing.T) {
158166
// Create temporary SQL file
159167
sqlFile := filepath.Join(tmpDir, "test.sql")
@@ -240,6 +248,16 @@ func TestExecuteFileScript(t *testing.T) {
240248
cmdOpts.Start.Validate = false
241249
cmdOpts.Start.Replace = false
242250

251+
mockPool.ExpectQuery("SELECT EXISTS").
252+
WithArgs("test_chain").
253+
WillReturnRows(pgxmock.NewRows([]string{"exists"}).AddRow(false))
254+
mockPool.ExpectQuery("INSERT INTO timetable\\.chain").
255+
WithArgs(anyArgs(9)...).
256+
WillReturnRows(pgxmock.NewRows([]string{"chain_id"}).AddRow(1))
257+
mockPool.ExpectQuery("INSERT INTO timetable\\.task").
258+
WithArgs(anyArgs(10)...).
259+
WillReturnRows(pgxmock.NewRows([]string{"task_id"}).AddRow(1))
260+
243261
err = mockpge.ExecuteFileScript(context.Background(), cmdOpts)
244262
assert.NoError(t, err)
245263
})
@@ -263,7 +281,7 @@ func TestExecuteFileScript(t *testing.T) {
263281
assert.NoError(t, err)
264282
})
265283

266-
t.Run("File without extension - YAML content", func(t *testing.T) {
284+
t.Run("File without extension", func(t *testing.T) {
267285
// Create file without extension with YAML content
268286
noExtFile := filepath.Join(tmpDir, "test_no_ext")
269287
yamlContent := `chains:
@@ -278,92 +296,17 @@ func TestExecuteFileScript(t *testing.T) {
278296
cmdOpts.Start.File = noExtFile
279297

280298
err = mockpge.ExecuteFileScript(context.Background(), cmdOpts)
281-
assert.NoError(t, err)
282-
})
283-
284-
t.Run("File without extension - SQL content", func(t *testing.T) {
285-
// Create file without extension with SQL content
286-
noExtFile := filepath.Join(tmpDir, "test_no_ext_sql")
287-
sqlContent := "SELECT 1;"
288-
err := os.WriteFile(noExtFile, []byte(sqlContent), 0644)
289-
assert.NoError(t, err)
290-
291-
// Mock the SQL execution
292-
mockPool.ExpectExec("SELECT 1;").WillReturnResult(pgxmock.NewResult("SELECT", 1))
293-
294-
cmdOpts := config.CmdOptions{}
295-
cmdOpts.Start.File = noExtFile
296-
297-
err = mockpge.ExecuteFileScript(context.Background(), cmdOpts)
298-
assert.NoError(t, err)
299-
})
300-
301-
t.Run("File not found error - SQL file", func(t *testing.T) {
302-
cmdOpts := config.CmdOptions{}
303-
cmdOpts.Start.File = "/nonexistent/file.sql"
304-
305-
err := mockpge.ExecuteFileScript(context.Background(), cmdOpts)
306299
assert.Error(t, err)
307300
})
308301

309-
t.Run("File not found error - unknown extension", func(t *testing.T) {
302+
t.Run("File not found error", func(t *testing.T) {
310303
cmdOpts := config.CmdOptions{}
311-
cmdOpts.Start.File = "/nonexistent/file.txt"
304+
cmdOpts.Start.File = "/nonexistent/file.sql"
312305

313306
err := mockpge.ExecuteFileScript(context.Background(), cmdOpts)
314307
assert.Error(t, err)
315308
})
316309

317-
t.Run("Unknown file extension defaults to content detection", func(t *testing.T) {
318-
// Create file with unknown extension containing SQL
319-
unknownFile := filepath.Join(tmpDir, "test.unknown")
320-
sqlContent := "SELECT 2;"
321-
err := os.WriteFile(unknownFile, []byte(sqlContent), 0644)
322-
assert.NoError(t, err)
323-
324-
// Mock the SQL execution
325-
mockPool.ExpectExec("SELECT 2;").WillReturnResult(pgxmock.NewResult("SELECT", 1))
326-
327-
cmdOpts := config.CmdOptions{}
328-
cmdOpts.Start.File = unknownFile
329-
330-
err = mockpge.ExecuteFileScript(context.Background(), cmdOpts)
331-
assert.NoError(t, err)
332-
})
333-
334-
t.Run("Empty file", func(t *testing.T) {
335-
// Create empty file
336-
emptyFile := filepath.Join(tmpDir, "empty.txt")
337-
err := os.WriteFile(emptyFile, []byte(""), 0644)
338-
assert.NoError(t, err)
339-
340-
// Mock empty SQL execution
341-
mockPool.ExpectExec("").WillReturnResult(pgxmock.NewResult("SELECT", 0))
342-
343-
cmdOpts := config.CmdOptions{}
344-
cmdOpts.Start.File = emptyFile
345-
346-
err = mockpge.ExecuteFileScript(context.Background(), cmdOpts)
347-
assert.NoError(t, err)
348-
})
349-
350-
t.Run("YAML file with whitespace prefix", func(t *testing.T) {
351-
// Create file with leading whitespace before chains:
352-
whitespaceFile := filepath.Join(tmpDir, "whitespace")
353-
yamlContent := `
354-
355-
chains:
356-
- name: test_chain`
357-
err := os.WriteFile(whitespaceFile, []byte(yamlContent), 0644)
358-
assert.NoError(t, err)
359-
360-
cmdOpts := config.CmdOptions{}
361-
cmdOpts.Start.File = whitespaceFile
362-
363-
err = mockpge.ExecuteFileScript(context.Background(), cmdOpts)
364-
assert.NoError(t, err)
365-
})
366-
367310
t.Run("YAML import with replace flag", func(t *testing.T) {
368311
// Create temporary YAML file
369312
yamlFile := filepath.Join(tmpDir, "test_replace.yaml")
@@ -380,14 +323,6 @@ chains:
380323
cmdOpts.Start.Validate = false
381324
cmdOpts.Start.Replace = true
382325

383-
anyArgs := func(i int) []any {
384-
args := make([]any, i)
385-
for j := range i {
386-
args[j] = pgxmock.AnyArg()
387-
}
388-
return args
389-
}
390-
391326
mockPool.ExpectExec("SELECT timetable\\.delete_job").
392327
WithArgs("test_chain_replace").
393328
WillReturnResult(pgxmock.NewResult("DELETE", 1))
@@ -424,22 +359,4 @@ INSERT INTO test VALUES (1);`
424359
assert.NoError(t, err)
425360
})
426361

427-
t.Run("Content detection with mixed content", func(t *testing.T) {
428-
// Create file with content that doesn't start with "chains:"
429-
mixedFile := filepath.Join(tmpDir, "mixed_content")
430-
mixedContent := `# This is a comment
431-
# chains: this is just a comment, not actual YAML
432-
SELECT 1;`
433-
err := os.WriteFile(mixedFile, []byte(mixedContent), 0644)
434-
assert.NoError(t, err)
435-
436-
// Mock the SQL execution since it doesn't start with "chains:" - use regex pattern
437-
mockPool.ExpectExec(`# This is a comment.*# chains:.*SELECT 1;`).WillReturnResult(pgxmock.NewResult("SELECT", 1))
438-
439-
cmdOpts := config.CmdOptions{}
440-
cmdOpts.Start.File = mixedFile
441-
442-
err = mockpge.ExecuteFileScript(context.Background(), cmdOpts)
443-
assert.NoError(t, err)
444-
})
445362
}

internal/pgengine/yaml.go

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ package pgengine
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"os"
7-
"path/filepath"
88
"strings"
99

1010
"gopkg.in/yaml.v3"
@@ -119,15 +119,21 @@ func (pge *PgEngine) createChainFromYaml(ctx context.Context, yamlChain *YamlCha
119119

120120
// Insert parameters if any
121121
if len(task.Parameters) > 0 {
122-
params, err := task.ToSQLParameters()
123-
if err != nil {
124-
return 0, fmt.Errorf("failed to convert parameters: %w", err)
125-
}
126-
_, err = pge.ConfigDb.Exec(ctx,
127-
"INSERT INTO timetable.parameter (task_id, order_id, value) VALUES ($1, 1, $2::jsonb)",
128-
taskID, params)
129-
if err != nil {
130-
return 0, fmt.Errorf("failed to insert parameters: %w", err)
122+
for paramIndex, param := range task.Parameters {
123+
orderID := paramIndex + 1
124+
125+
// Convert parameter to JSON for JSONB storage
126+
jsonValue, err := json.Marshal(param)
127+
if err != nil {
128+
return 0, fmt.Errorf("failed to marshal parameter %d to JSON: %w", orderID, err)
129+
}
130+
131+
_, err = pge.ConfigDb.Exec(ctx,
132+
"INSERT INTO timetable.parameter (task_id, order_id, value) VALUES ($1, $2, $3::jsonb)",
133+
taskID, orderID, string(jsonValue))
134+
if err != nil {
135+
return 0, fmt.Errorf("failed to insert parameter %d: %w", orderID, err)
136+
}
131137
}
132138
}
133139
}
@@ -229,12 +235,6 @@ func ParseYamlFile(filePath string) (*YamlConfig, error) {
229235
return nil, fmt.Errorf("file not found: %s", filePath)
230236
}
231237

232-
// Check file extension
233-
ext := strings.ToLower(filepath.Ext(filePath))
234-
if ext != ".yaml" && ext != ".yml" {
235-
return nil, fmt.Errorf("file must have .yaml or .yml extension: %s", filePath)
236-
}
237-
238238
// Read file
239239
data, err := os.ReadFile(filePath)
240240
if err != nil {
@@ -259,28 +259,4 @@ func ParseYamlFile(filePath string) (*YamlConfig, error) {
259259
return &config, nil
260260
}
261261

262-
// ToSQLParameters converts YAML parameters to SQL-compatible format
263-
func (t *YamlTask) ToSQLParameters() (string, error) {
264-
if len(t.Parameters) == 0 {
265-
return "", nil
266-
}
267-
268-
// Convert to JSON array format for PostgreSQL
269-
params := make([]string, len(t.Parameters))
270-
for i, param := range t.Parameters {
271-
switch v := param.(type) {
272-
case string:
273-
params[i] = fmt.Sprintf(`"%s"`, strings.ReplaceAll(v, `"`, `\"`))
274-
case int, int32, int64:
275-
params[i] = fmt.Sprintf("%v", v)
276-
case float32, float64:
277-
params[i] = fmt.Sprintf("%v", v)
278-
case bool:
279-
params[i] = fmt.Sprintf("%t", v)
280-
default:
281-
params[i] = fmt.Sprintf(`"%v"`, v)
282-
}
283-
}
284262

285-
return fmt.Sprintf("[%s]", strings.Join(params, ", ")), nil
286-
}

0 commit comments

Comments
 (0)