Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/limen/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ type Driver interface {
IntrospectColumnsQuery(tableName string) (string, []any)
IntrospectIndexesQuery(tableName string) (string, []any)
IntrospectForeignKeysQuery(tableName string) (string, []any)
// QuoteIdentifier quotes a table, column, index or constraint name for this
// database, so that reserved words and case-sensitive names survive into
// the generated DDL. Must agree with how the runtime adapter quotes.
QuoteIdentifier(name string) string
MapGoTypeToSQL(goType limen.ColumnType, isAutoIncrement bool) string
MapSQLTypeToGoType(dataType string) limen.ColumnType
GetAutoIncrementSuffix() string
Expand Down
5 changes: 0 additions & 5 deletions cmd/limen/driver_base.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main

import (
"fmt"
"strings"

"github.com/thecodearcher/limen"
Expand Down Expand Up @@ -42,10 +41,6 @@ func (d *baseDriver) ParseForeignKeyRow(scan func(dest ...any) error) (limen.For
}, nil
}

func (d *baseDriver) DropColumnSQL(tableName, columnName string) string {
return fmt.Sprintf("DROP COLUMN %s", columnName)
}

func (d *baseDriver) ParseColumnRow(scan func(dest ...any) error) (limen.ColumnDefinition, error) {
var colName string

Expand Down
26 changes: 24 additions & 2 deletions cmd/limen/driver_mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ func NewMySQLDriver() Driver {
return &mysqlDriver{}
}

// mysqlQuoteChar is MySQL's and MariaDB's identifier quote. It is accepted in
// every sql_mode, including ANSI_QUOTES, where " also becomes an identifier
// quote.
const mysqlQuoteChar = "`"

// QuoteIdentifier quotes a table, column, index or constraint name, doubling
// any embedded backtick so the result stays a single identifier. It mirrors
// adapters/sql's quoteIdent, since the generator and the runtime adapter must
// agree on the name a table actually has.
//
// Hand-rolled because go-sql-driver/mysql exports no equivalent of pgx's
// Identifier.Sanitize.
func (d *mysqlDriver) QuoteIdentifier(name string) string {
return mysqlQuoteChar +
strings.ReplaceAll(name, mysqlQuoteChar, mysqlQuoteChar+mysqlQuoteChar) +
mysqlQuoteChar
}

func (d *mysqlDriver) Name() string {
return string(DriverMySQL)
}
Expand Down Expand Up @@ -133,9 +151,13 @@ func (d *mysqlDriver) FormatDefaultValue(defaultValue string) string {
}

func (d *mysqlDriver) DropIndexSQL(tableName, indexName string) string {
return fmt.Sprintf("DROP INDEX %s ON %s", indexName, tableName)
return fmt.Sprintf("DROP INDEX %s ON %s", d.QuoteIdentifier(indexName), d.QuoteIdentifier(tableName))
}

func (d *mysqlDriver) DropForeignKeySQL(tableName, constraintName string) string {
return fmt.Sprintf("DROP FOREIGN KEY %s", constraintName)
return fmt.Sprintf("DROP FOREIGN KEY %s", d.QuoteIdentifier(constraintName))
}

func (d *mysqlDriver) DropColumnSQL(tableName, columnName string) string {
return fmt.Sprintf("DROP COLUMN %s", d.QuoteIdentifier(columnName))
}
16 changes: 14 additions & 2 deletions cmd/limen/driver_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"strings"

"github.com/jackc/pgx/v5"
_ "github.com/jackc/pgx/v5/stdlib"

"github.com/thecodearcher/limen"
Expand All @@ -19,6 +20,13 @@ func NewPostgresDriver() Driver {
return &postgresDriver{}
}

// QuoteIdentifier defers to pgx, which is already this driver's connection
// library, rather than re-deriving PostgreSQL's quoting rules. Beyond doubling
// embedded quotes it also strips NUL bytes, which PostgreSQL rejects outright.
func (d *postgresDriver) QuoteIdentifier(name string) string {
return pgx.Identifier{name}.Sanitize()
}

func (d *postgresDriver) Name() string {
return string(DriverPostgres)
}
Expand Down Expand Up @@ -182,9 +190,13 @@ func (d *postgresDriver) FormatDefaultValue(defaultValue string) string {
}

func (d *postgresDriver) DropIndexSQL(tableName, indexName string) string {
return fmt.Sprintf("DROP INDEX IF EXISTS %s", indexName)
return fmt.Sprintf("DROP INDEX IF EXISTS %s", d.QuoteIdentifier(indexName))
}

func (d *postgresDriver) DropForeignKeySQL(tableName, constraintName string) string {
return fmt.Sprintf("DROP CONSTRAINT %s", constraintName)
return fmt.Sprintf("DROP CONSTRAINT %s", d.QuoteIdentifier(constraintName))
}

func (d *postgresDriver) DropColumnSQL(tableName, columnName string) string {
return fmt.Sprintf("DROP COLUMN %s", d.QuoteIdentifier(columnName))
}
54 changes: 32 additions & 22 deletions cmd/limen/migration_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,28 @@ func (s *sqlMigrationGenerator) generateDownMigration(schema *limen.SchemaDefini
if diff != nil && diff.HasChanges() {
return s.generateAlterDownMigration(schema.GetTableName(), diff)
}
return fmt.Sprintf("DROP TABLE IF EXISTS %s;", schema.GetTableName()), nil
return fmt.Sprintf("DROP TABLE IF EXISTS %s;", s.quote(string(schema.GetTableName()))), nil
}

// quote quotes a single identifier for the target database.
func (s *sqlMigrationGenerator) quote(name string) string {
return s.driver.QuoteIdentifier(name)
}

// quoteAll quotes each identifier and joins them with separator, for the
// column lists in PRIMARY KEY and CREATE INDEX.
func quoteAll[T ~string](s *sqlMigrationGenerator, names []T, separator string) string {
quoted := make([]string, len(names))
for i := range names {
quoted[i] = s.quote(string(names[i]))
}
return strings.Join(quoted, separator)
}

func (s *sqlMigrationGenerator) generateCreateTable(schema *limen.SchemaDefinition) (string, error) {
var buf strings.Builder

fmt.Fprintf(&buf, "CREATE TABLE IF NOT EXISTS %s (\n", schema.GetTableName())
fmt.Fprintf(&buf, "CREATE TABLE IF NOT EXISTS %s (\n", s.quote(string(schema.GetTableName())))

columns := make([]string, 0, len(schema.Columns))
for _, field := range schema.Columns {
Expand All @@ -61,7 +76,7 @@ func (s *sqlMigrationGenerator) generateCreateTable(schema *limen.SchemaDefiniti
}

if len(pkFields) > 0 {
fmt.Fprintf(&buf, ",\n PRIMARY KEY (%s)", strings.Join(pkFields, ", "))
fmt.Fprintf(&buf, ",\n PRIMARY KEY (%s)", quoteAll(s, pkFields, ", "))
}

for _, fk := range schema.ForeignKeys {
Expand Down Expand Up @@ -107,7 +122,11 @@ func (s *sqlMigrationGenerator) generateAlterDownMigration(tableName limen.Schem
statements := []string{}

for _, idx := range diff.AddedIndexes {
dropSQL := s.driver.DropIndexSQL(string(tableName), idx.Name)
// DropIndexSQL is a standalone statement but carries no terminator,
// unlike DropColumnSQL and DropForeignKeySQL, which are fragments of
// the ALTER TABLE appended below. Without the semicolon the two run
// together into one statement the database rejects.
dropSQL := s.driver.DropIndexSQL(string(tableName), idx.Name) + ";"
statements = append(statements, dropSQL)
}

Expand Down Expand Up @@ -135,7 +154,7 @@ func (s *sqlMigrationGenerator) generateUpAlterTableStatement(tableName limen.Sc
var buf strings.Builder
statements := []string{}

fmt.Fprintf(&buf, "ALTER TABLE %s\n", tableName)
fmt.Fprintf(&buf, "ALTER TABLE %s\n", s.quote(string(tableName)))
for _, col := range diff.AddedColumns {
colDef := s.generateColumnDefinition(&col)
statements = append(statements, fmt.Sprintf("ADD COLUMN %s", colDef))
Expand All @@ -161,7 +180,7 @@ func (s *sqlMigrationGenerator) generateDownAlterTableStatement(tableName limen.
var buf strings.Builder
statements := []string{}

fmt.Fprintf(&buf, "ALTER TABLE %s\n", tableName)
fmt.Fprintf(&buf, "ALTER TABLE %s\n", s.quote(string(tableName)))

for _, fk := range diff.AddedForeignKeys {
dropOp := s.driver.DropForeignKeySQL(string(tableName), fk.Name)
Expand All @@ -181,7 +200,7 @@ func (s *sqlMigrationGenerator) generateDownAlterTableStatement(tableName limen.
}

func (s *sqlMigrationGenerator) generateColumnDefinition(field *limen.ColumnDefinition) string {
parts := []string{field.Name}
parts := []string{s.quote(field.Name)}

isAutoIncrement := field.LogicalField == limen.SchemaIDField && s.useAutoIncrementIDs

Expand Down Expand Up @@ -214,10 +233,12 @@ func (s *sqlMigrationGenerator) generateForeignKeyStatement(fk *limen.ForeignKey

if alterTable {
fmt.Fprintf(&buf, "ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)",
fk.Name, fk.Column, string(fk.ReferencedSchema), string(fk.ReferencedField))
s.quote(fk.Name), s.quote(string(fk.Column)),
s.quote(string(fk.ReferencedSchema)), s.quote(string(fk.ReferencedField)))
} else {
fmt.Fprintf(&buf, ",\nCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)",
fk.Name, fk.Column, string(fk.ReferencedSchema), string(fk.ReferencedField))
s.quote(fk.Name), s.quote(string(fk.Column)),
s.quote(string(fk.ReferencedSchema)), s.quote(string(fk.ReferencedField)))
}

if fk.OnDelete != "" {
Expand All @@ -234,20 +255,9 @@ func (s *sqlMigrationGenerator) generateForeignKeyStatement(fk *limen.ForeignKey
func (s *sqlMigrationGenerator) generateCreateIndexStatement(idx *limen.IndexDefinition, tableName limen.SchemaTableName) string {
if idx.Unique {
return fmt.Sprintf("CREATE UNIQUE INDEX %s ON %s (%s);",
idx.Name, tableName, joinCustomStringSlice(idx.Columns, ", "))
s.quote(idx.Name), s.quote(string(tableName)), quoteAll(s, idx.Columns, ", "))
}

return fmt.Sprintf("CREATE INDEX %s ON %s (%s);",
idx.Name, tableName, joinCustomStringSlice(idx.Columns, ", "))
}

func joinCustomStringSlice[T ~string](fields []T, separator string) string {
var joined string
for i := range fields {
joined += string(fields[i])
if i < len(fields)-1 {
joined += separator
}
}
return joined
s.quote(idx.Name), s.quote(string(tableName)), quoteAll(s, idx.Columns, ", "))
}
Loading