diff --git a/rivershared/sqlctemplate/sqlc_template.go b/rivershared/sqlctemplate/sqlc_template.go index e0a02d6a..965ff46d 100644 --- a/rivershared/sqlctemplate/sqlc_template.go +++ b/rivershared/sqlctemplate/sqlc_template.go @@ -97,6 +97,12 @@ var ( templateRE = regexp.MustCompile(`/\* TEMPLATE: (.*?) \*/`) ) +// Regex to search for in SQL after replacement has occurred and which probably +// represents a syntax error. sqlctemplate isn't a true compiler so if template +// REs don't match, we can be left with some subtle bugs where there's some +// minor problem like a missing semicolon that are hard to debug. +var postReplaceMistakeRE = regexp.MustCompile(`\/\*\s*TEMPLATE([A-Z0-9_]+)?`) // also finds "/* TEMPLATE_BEGIN" + // Run replaces any tempates in input SQL with values from context added via // WithReplacements. // @@ -189,6 +195,11 @@ func (r *Replacer) RunSafely(ctx context.Context, sql string, args []any) (strin return "", nil, errors.New("sqlctemplate params present in SQL but missing in context: " + strings.Join(templatesMissing, ", ")) } + probableMistakes := postReplaceMistakeRE.FindAllString(updatedSQL, -1) + if len(probableMistakes) > 0 { + return "", nil, errors.New("sqlctemplate found template-like tag after replacements; probably syntax error or missing end tag: " + strings.Join(probableMistakes, ", ")) + } + if len(container.NamedArgs) > 0 { placeholderNum := len(args) diff --git a/rivershared/sqlctemplate/sqlc_template_test.go b/rivershared/sqlctemplate/sqlc_template_test.go index 5fd0c612..38213b55 100644 --- a/rivershared/sqlctemplate/sqlc_template_test.go +++ b/rivershared/sqlctemplate/sqlc_template_test.go @@ -115,6 +115,53 @@ func TestReplacer(t *testing.T) { `, updatedSQL) }) + t.Run("SyntaxMistakes", func(t *testing.T) { + t.Parallel() + + replacer, _ := setup(t) + + // Missing colon after "TEMPLATE". + { + ctx := WithReplacements(ctx, map[string]Replacement{}, nil) + + _, _, err := replacer.RunSafely(ctx, ` + -- name: JobCountByState :one + SELECT count(*) + FROM /* TEMPLATE schema */river_job + WHERE state = @state; + `, nil) + require.EqualError(t, err, "sqlctemplate found template-like tag after replacements; probably syntax error or missing end tag: /* TEMPLATE") + } + + // Missing "TEMPLATE_END". + { + ctx := WithReplacements(ctx, map[string]Replacement{}, nil) + + _, _, err := replacer.RunSafely(ctx, ` + -- name: JobCountByState :one + SELECT count(*) + FROM /* TEMPLATE_BEGIN: schema */river_job + WHERE state = @state; + `, nil) + require.EqualError(t, err, "sqlctemplate found template-like tag after replacements; probably syntax error or missing end tag: /* TEMPLATE_BEGIN") + } + + // Extra whitespace before "TEMPLATE". + { + ctx := WithReplacements(ctx, map[string]Replacement{ + "state": {Value: "'available'"}, + }, nil) + + _, _, err := replacer.RunSafely(ctx, ` + -- name: JobCountByState :one + SELECT count(*) + FROM /* TEMPLATE schema */river_job + WHERE state = /* TEMPLATE_BEGIN: state */ 'available' /* TEMPLATE_END */ -- need to have one valid template to get to the right error + `, nil) + require.EqualError(t, err, "sqlctemplate found template-like tag after replacements; probably syntax error or missing end tag: /* TEMPLATE") + } + }) + t.Run("RepeatedTemplate", func(t *testing.T) { t.Parallel()