Skip to content
Merged
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
18 changes: 9 additions & 9 deletions pkg/dialect/dialect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func TestPostgresDialect(t *testing.T) {
if got := d.Name(); got != "postgres" {
t.Fatalf("unexpected name: %q", got)
}
if got := d.Features(); got != FeatureInsertReturning|FeatureUpdateReturning|FeatureDeleteReturning|FeatureOffset|FeatureUpsert|FeatureCTE|FeatureDefaultPlaceholder|FeatureSavepoint|FeatureSelectLocking|FeatureNullsOrder|FeatureSelectDistinctOn {
if got := d.Features(); got != FeatureInsertReturning|FeatureUpdateReturning|FeatureDeleteReturning|FeatureOffset|FeatureUpsert|FeatureCTE|FeatureDefaultPlaceholder|FeatureSavepoint|FeatureSelectLocking|FeatureNullsOrder|FeatureSelectDistinctOn|FeatureUnlimited {
t.Fatalf("unexpected features: %b", got)
}
if got := d.QuoteIdentifier(`user"name`); got != `"user""name"` {
Expand Down Expand Up @@ -193,10 +193,10 @@ func TestPostgresDialect(t *testing.T) {
if got := d.LimitOffset(10, 0); got != "LIMIT 10" {
t.Fatalf("unexpected limit only clause: %q", got)
}
if got := d.LimitOffset(0, 20); got != "OFFSET 20" {
if got := d.LimitOffset(0, 20); got != "LIMIT 0 OFFSET 20" {
t.Fatalf("unexpected offset only clause: %q", got)
}
if got := d.LimitOffset(0, 0); got != "" {
if got := d.LimitOffset(0, 0); got != "LIMIT 0" {
t.Fatalf("unexpected empty limit clause: %q", got)
}
if got := d.UpsertClause("users", []string{"email"}, []string{"name"}); got != "ON CONFLICT DO UPDATE" {
Expand Down Expand Up @@ -224,7 +224,7 @@ func TestMySQLDialect(t *testing.T) {
if got := d.Name(); got != "mysql" {
t.Fatalf("unexpected name: %q", got)
}
if got := d.Features(); got != FeatureOffset|FeatureUpsert|FeatureSavepoint|FeatureSelectLocking {
if got := d.Features(); got != FeatureOffset|FeatureUpsert|FeatureSavepoint|FeatureSelectLocking|FeatureCTE|FeatureUpdateOrder|FeatureUpdateLimit|FeatureDeleteOrder|FeatureDeleteLimit|FeatureUnlimited {
t.Fatalf("unexpected features: %b", got)
}
if got := d.QuoteIdentifier("user`name"); got != "`user``name`" {
Expand Down Expand Up @@ -283,10 +283,10 @@ func TestMySQLDialect(t *testing.T) {
if got := d.LimitOffset(10, 0); got != "LIMIT 10" {
t.Fatalf("unexpected limit only clause: %q", got)
}
if got := d.LimitOffset(0, 20); got != "LIMIT 18446744073709551615 OFFSET 20" {
if got := d.LimitOffset(0, 20); got != "LIMIT 20, 0" {
t.Fatalf("unexpected offset only clause: %q", got)
}
if got := d.LimitOffset(0, 0); got != "" {
if got := d.LimitOffset(0, 0); got != "LIMIT 0" {
t.Fatalf("unexpected empty limit clause: %q", got)
}
if got := d.UpsertClause("users", []string{"email"}, []string{"name"}); got != "ON DUPLICATE KEY UPDATE" {
Expand Down Expand Up @@ -314,7 +314,7 @@ func TestSQLiteDialect(t *testing.T) {
if got := d.Name(); got != "sqlite" {
t.Fatalf("unexpected name: %q", got)
}
if got := d.Features(); got != FeatureInsertReturning|FeatureUpdateReturning|FeatureDeleteReturning|FeatureOffset|FeatureUpsert|FeatureSavepoint|FeatureNullsOrder {
if got := d.Features(); got != FeatureInsertReturning|FeatureUpdateReturning|FeatureDeleteReturning|FeatureOffset|FeatureUpsert|FeatureSavepoint|FeatureNullsOrder|FeatureCTE|FeatureUpdateOrder|FeatureUpdateLimit|FeatureDeleteOrder|FeatureDeleteLimit|FeatureUnlimited {
t.Fatalf("unexpected features: %b", got)
}
if got := d.QuoteIdentifier(`user"name`); got != `"user""name"` {
Expand Down Expand Up @@ -368,10 +368,10 @@ func TestSQLiteDialect(t *testing.T) {
if got := d.LimitOffset(10, 0); got != "LIMIT 10" {
t.Fatalf("unexpected limit only clause: %q", got)
}
if got := d.LimitOffset(0, 20); got != "LIMIT -1 OFFSET 20" {
if got := d.LimitOffset(0, 20); got != "LIMIT 0 OFFSET 20" {
t.Fatalf("unexpected offset only clause: %q", got)
}
if got := d.LimitOffset(0, 0); got != "" {
if got := d.LimitOffset(0, 0); got != "LIMIT 0" {
t.Fatalf("unexpected empty limit clause: %q", got)
}
if got := d.UpsertClause("users", []string{"email"}, []string{"name"}); got != "ON CONFLICT DO UPDATE" {
Expand Down
5 changes: 5 additions & 0 deletions pkg/dialect/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const (
FeatureSelectLocking
FeatureNullsOrder
FeatureSelectDistinctOn
FeatureUpdateOrder
FeatureUpdateLimit
FeatureDeleteOrder
FeatureDeleteLimit
FeatureUnlimited
)

// HasFeature reports whether a feature set includes the requested capability.
Expand Down
13 changes: 11 additions & 2 deletions pkg/dialect/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ func (d *MySQLDialect) Name() string {

// Features returns MySQL capabilities supported by Rain.
func (d *MySQLDialect) Features() Feature {
return FeatureOffset | FeatureUpsert | FeatureSavepoint | FeatureSelectLocking
return FeatureOffset |
FeatureUpsert |
FeatureSavepoint |
FeatureSelectLocking |
FeatureCTE |
FeatureUpdateOrder |
FeatureUpdateLimit |
FeatureDeleteOrder |
FeatureDeleteLimit |
FeatureUnlimited
}

// QuoteIdentifier quotes identifiers with backticks.
Expand Down Expand Up @@ -93,7 +102,7 @@ func (d *MySQLDialect) AutoIncrementKeyword() string {

// LimitOffset returns MySQL LIMIT/OFFSET syntax.
func (d *MySQLDialect) LimitOffset(limit, offset int) string {
if limit > 0 {
if limit >= 0 {
if offset > 0 {
return "LIMIT " + strconv.Itoa(offset) + ", " + strconv.Itoa(limit)
}
Expand Down
11 changes: 6 additions & 5 deletions pkg/dialect/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ func (d *PostgresDialect) Features() Feature {
FeatureSavepoint |
FeatureSelectLocking |
FeatureNullsOrder |
FeatureSelectDistinctOn
FeatureSelectDistinctOn |
FeatureUnlimited
}

// QuoteIdentifier quotes identifiers with double quotes.
Expand Down Expand Up @@ -104,10 +105,10 @@ func (d *PostgresDialect) AutoIncrementKeyword() string {

// LimitOffset returns PostgreSQL LIMIT/OFFSET syntax.
func (d *PostgresDialect) LimitOffset(limit, offset int) string {
if limit > 0 && offset > 0 {
return "LIMIT " + strconv.Itoa(limit) + " OFFSET " + strconv.Itoa(offset)
}
if limit > 0 {
if limit >= 0 {
if offset > 0 {
return "LIMIT " + strconv.Itoa(limit) + " OFFSET " + strconv.Itoa(offset)
}
return "LIMIT " + strconv.Itoa(limit)
}
if offset > 0 {
Expand Down
10 changes: 8 additions & 2 deletions pkg/dialect/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ func (d *SQLiteDialect) Features() Feature {
FeatureOffset |
FeatureUpsert |
FeatureSavepoint |
FeatureNullsOrder
FeatureNullsOrder |
FeatureCTE |
FeatureUpdateOrder |
FeatureUpdateLimit |
FeatureDeleteOrder |
FeatureDeleteLimit |
FeatureUnlimited
}

// QuoteIdentifier quotes identifiers with double quotes.
Expand Down Expand Up @@ -76,7 +82,7 @@ func (d *SQLiteDialect) AutoIncrementKeyword() string {

// LimitOffset returns SQLite LIMIT/OFFSET syntax.
func (d *SQLiteDialect) LimitOffset(limit, offset int) string {
if limit > 0 {
if limit >= 0 {
if offset > 0 {
return "LIMIT " + strconv.Itoa(limit) + " OFFSET " + strconv.Itoa(offset)
}
Expand Down
83 changes: 83 additions & 0 deletions pkg/rain/query_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,86 @@ func closeRows(rows *sql.Rows, errp *error) {
*errp = err
}
}

func writeCTEs(ctx *compileContext, ctes []cteDefinition, label string) error {
if len(ctes) == 0 || ctx.skipCTEs {
return nil
}
if !dialect.HasFeature(ctx.dialect.Features(), dialect.FeatureCTE) {
return fmt.Errorf("rain: %s queries do not support CTEs for %s dialect", label, ctx.dialect.Name())
}
ctx.writeString("WITH ")
for idx, cte := range ctes {
if idx > 0 {
ctx.writeString(", ")
}
if strings.TrimSpace(cte.name) == "" {
return errors.New("rain: CTE name cannot be empty")
}
if cte.query == nil {
return fmt.Errorf("rain: CTE %q requires a query", cte.name)
}
if len(cte.query.ctes) > 0 {
return fmt.Errorf("rain: CTE %q body cannot itself contain CTEs", cte.name)
}
ctx.writeQuotedIdentifier(cte.name)
ctx.writeString(" AS (")
if err := cte.query.writeSQL(ctx); err != nil {
return err
}
ctx.writeByte(')')
}
ctx.writeByte(' ')
return nil
}

func writeOrderLimit(ctx *compileContext, order []schema.OrderExpr, limit *int, offset *int, featureOrder, featureLimit dialect.Feature) error {
if len(order) > 0 {
if featureOrder != dialect.FeatureUnlimited && !dialect.HasFeature(ctx.dialect.Features(), featureOrder) {
return fmt.Errorf("rain: ORDER BY is not supported for this query type in %s dialect", ctx.dialect.Name())
}
ctx.writeString(" ORDER BY ")
for idx, item := range order {
if idx > 0 {
ctx.writeString(", ")
}
if err := ctx.writeExpression(item.Expr); err != nil {
return err
}
ctx.writeByte(' ')
ctx.writeString(string(item.Direction))
if item.NullsOrder != "" {
if !dialect.HasFeature(ctx.dialect.Features(), dialect.FeatureNullsOrder) {
return fmt.Errorf("rain: NULLS FIRST/LAST is not supported by %s dialect", ctx.dialect.Name())
}
ctx.writeByte(' ')
ctx.writeString(string(item.NullsOrder))
}
}
}

if limit != nil || (offset != nil && *offset > 0) {
if featureLimit != dialect.FeatureUnlimited && !dialect.HasFeature(ctx.dialect.Features(), featureLimit) {
return fmt.Errorf("rain: LIMIT/OFFSET is not supported for this query type in %s dialect", ctx.dialect.Name())
}
l := -1
if limit != nil {
l = *limit
if l < 0 {
return errors.New("rain: LIMIT must be non-negative")
}
}
o := 0
if offset != nil {
o = *offset
if o < 0 {
return errors.New("rain: OFFSET must be non-negative")
}
}
if clause := ctx.dialect.LimitOffset(l, o); clause != "" {
ctx.writeByte(' ')
ctx.writeString(clause)
}
}
return nil
}
32 changes: 32 additions & 0 deletions pkg/rain/query_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type DeleteQuery struct {
dialect dialect.Dialect
table *schema.TableDef
where []schema.Predicate
order []schema.OrderExpr
limit *int
ctes []cteDefinition
returning []schema.Expression
unbounded bool
}
Expand All @@ -38,6 +41,26 @@ func (q *DeleteQuery) Returning(exprs ...schema.Expression) *DeleteQuery {
return q
}

// With appends a common table expression definition.
func (q *DeleteQuery) With(name string, query *SelectQuery) *DeleteQuery {
q.ctes = append(q.ctes, cteDefinition{name: name, query: query})
return q
}

// OrderBy appends ORDER BY expressions.
// Supported by MySQL and SQLite.
func (q *DeleteQuery) OrderBy(order ...schema.OrderExpr) *DeleteQuery {
q.order = append(q.order, order...)
return q
}

// Limit sets the LIMIT clause.
// Supported by MySQL and SQLite.
func (q *DeleteQuery) Limit(limit int) *DeleteQuery {
q.limit = &limit
return q
}

// Unbounded allows DELETE without a WHERE clause.
func (q *DeleteQuery) Unbounded() *DeleteQuery {
q.unbounded = true
Expand All @@ -58,6 +81,11 @@ func (q *DeleteQuery) ToSQL() (string, []any, error) {

ctx := newCompileContext(q.dialect)
defer releaseCompileContext(ctx)

if err := writeCTEs(ctx, q.ctes, "delete"); err != nil {
return "", nil, err
}

ctx.writeString("DELETE FROM ")
ctx.writeTableName(q.table)
if len(q.where) > 0 {
Expand All @@ -67,6 +95,10 @@ func (q *DeleteQuery) ToSQL() (string, []any, error) {
}
}

if err := writeOrderLimit(ctx, q.order, q.limit, nil, dialect.FeatureDeleteOrder, dialect.FeatureDeleteLimit); err != nil {
return "", nil, err
}

if err := ctx.writeReturning(q.returning, q.returningClause()); err != nil {
return "", nil, err
}
Expand Down
Loading