Преглед на файлове

fix(executor): reject CHECK and FOREIGN KEY at DDL with 0A000

Both clauses were parsed and then discarded: the schema did not store them and
nothing validated writes, so applications believed a constraint existed while
orphan rows and invalid values were accepted. Reject inline and table-level
CHECK/FOREIGN KEY (including referential actions and named constraints) at
CREATE TABLE and ALTER TABLE ADD COLUMN with SQLSTATE 0A000 and point the client
at application-level enforcement. Cascades remain unimplemented.

Gogs and Vikunja declare neither clause, so their migrations are unaffected.
GoatCounter's retired SQLite schema does and is intentionally no longer
bootstrappable on this engine build.

Tasks: #61 #62
PizzaSQL Bot преди 1 ден
родител
ревизия
47dba61121
променени са 3 файла, в които са добавени 111 реда и са изтрити 7 реда
  1. 49 0
      pkg/executor/check_fk_test.go
  2. 41 7
      pkg/executor/executor.go
  3. 21 0
      pkg/pgserver/unsupported_constraints_test.go

+ 49 - 0
pkg/executor/check_fk_test.go

@@ -0,0 +1,49 @@
+package executor
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/danfragoso/pizzasql-next/pkg/pgerror"
+)
+
+func TestCheckConstraintsRejectedAtDDL(t *testing.T) {
+	_, schema, table := newTestDB(t)
+	e := newExec(schema, table)
+
+	for _, sql := range []string{
+		"CREATE TABLE t1 (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))",
+		"CREATE TABLE t2 (id INTEGER PRIMARY KEY, age INTEGER, CHECK (age > 0))",
+		"CREATE TABLE t3 (id INTEGER PRIMARY KEY, age INTEGER, CONSTRAINT positive_age CHECK (age > 0))",
+	} {
+		if _, err := execSQL(e, sql); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
+			t.Fatalf("%s: error = %v, want ErrFeatureNotSupported", sql, err)
+		}
+	}
+
+	execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
+	if _, err := execSQL(e, "ALTER TABLE t ADD COLUMN age INTEGER CHECK (age > 0)"); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
+		t.Fatalf("ALTER CHECK error = %v, want ErrFeatureNotSupported", err)
+	}
+}
+
+func TestForeignKeyConstraintsRejectedAtDDL(t *testing.T) {
+	_, schema, table := newTestDB(t)
+	e := newExec(schema, table)
+	execMust(t, e, "CREATE TABLE parent (id INTEGER PRIMARY KEY)")
+
+	for _, sql := range []string{
+		"CREATE TABLE child1 (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id))",
+		"CREATE TABLE child2 (id INTEGER PRIMARY KEY, parent_id INTEGER, FOREIGN KEY (parent_id) REFERENCES parent(id))",
+		"CREATE TABLE child3 (id INTEGER PRIMARY KEY, parent_id INTEGER, CONSTRAINT child_parent FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE CASCADE)",
+	} {
+		if _, err := execSQL(e, sql); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
+			t.Fatalf("%s: error = %v, want ErrFeatureNotSupported", sql, err)
+		}
+	}
+
+	execMust(t, e, "CREATE TABLE t (id INTEGER PRIMARY KEY)")
+	if _, err := execSQL(e, "ALTER TABLE t ADD COLUMN parent_id INTEGER REFERENCES parent(id)"); !errors.Is(err, pgerror.ErrFeatureNotSupported) {
+		t.Fatalf("ALTER FK error = %v, want ErrFeatureNotSupported", err)
+	}
+}

+ 41 - 7
pkg/executor/executor.go

@@ -13,6 +13,7 @@ import (
 	"github.com/danfragoso/pizzasql-next/pkg/analyzer"
 	"github.com/danfragoso/pizzasql-next/pkg/lexer"
 	"github.com/danfragoso/pizzasql-next/pkg/parser"
+	"github.com/danfragoso/pizzasql-next/pkg/pgerror"
 	"github.com/danfragoso/pizzasql-next/pkg/storage"
 	"github.com/danfragoso/pizzasql-next/pkg/version"
 )
@@ -3559,22 +3560,45 @@ func (e *Executor) executeCreateTable(stmt *parser.CreateTableStmt) (*Result, er
 				}
 			case parser.ConstraintAutoIncrement:
 				schema.AutoIncrement = true
+			case parser.ConstraintCheck:
+				return nil, pgerror.Unsupported(
+					"CHECK constraints are not supported: column %q declares CHECK; enforce the rule in the application (the engine parsed but never evaluated CHECK, so accepting it would silently allow invalid rows)",
+					col.Name,
+				)
+			case parser.ConstraintForeignKey:
+				return nil, pgerror.Unsupported(
+					"FOREIGN KEY constraints are not supported: column %q declares REFERENCES %s; enforce referential integrity in the application (the engine never validated foreign keys)",
+					col.Name, constraint.RefTable,
+				)
 			}
 		}
 
 		schema.Columns = append(schema.Columns, col)
 	}
 
-	// Handle table-level constraints
+	// Handle table-level constraints. CHECK and FOREIGN KEY are rejected: they
+	// were previously parsed and discarded, which let applications believe a
+	// constraint existed when nothing enforced it.
 	for _, constraint := range stmt.Constraints {
-		if constraint.Type == parser.ConstraintPrimaryKey && len(constraint.Columns) > 0 {
-			schema.PrimaryKey = constraint.Columns[0]
-			for i := range schema.Columns {
-				if strings.EqualFold(schema.Columns[i].Name, schema.PrimaryKey) {
-					schema.Columns[i].PrimaryKey = true
-					schema.Columns[i].Nullable = false
+		switch constraint.Type {
+		case parser.ConstraintPrimaryKey:
+			if len(constraint.Columns) > 0 {
+				schema.PrimaryKey = constraint.Columns[0]
+				for i := range schema.Columns {
+					if strings.EqualFold(schema.Columns[i].Name, schema.PrimaryKey) {
+						schema.Columns[i].PrimaryKey = true
+						schema.Columns[i].Nullable = false
+					}
 				}
 			}
+		case parser.ConstraintCheck:
+			return nil, pgerror.Unsupported(
+				"CHECK constraints are not supported: enforce the rule in the application instead (the engine parsed but never evaluated CHECK)",
+			)
+		case parser.ConstraintForeignKey:
+			return nil, pgerror.Unsupported(
+				"FOREIGN KEY constraints are not supported: enforce referential integrity in the application instead (the engine never validated foreign keys)",
+			)
 		}
 	}
 
@@ -3969,6 +3993,16 @@ func (e *Executor) executeAlterTableAddColumn(table string, action *parser.AddCo
 				}
 				col.Default = val
 			}
+		case parser.ConstraintCheck:
+			return nil, pgerror.Unsupported(
+				"CHECK constraints are not supported: column %q declares CHECK; enforce the rule in the application instead",
+				col.Name,
+			)
+		case parser.ConstraintForeignKey:
+			return nil, pgerror.Unsupported(
+				"FOREIGN KEY constraints are not supported: column %q declares REFERENCES %s; enforce referential integrity in the application instead",
+				col.Name, constraint.RefTable,
+			)
 		}
 	}
 

+ 21 - 0
pkg/pgserver/unsupported_constraints_test.go

@@ -0,0 +1,21 @@
+package pgserver
+
+import (
+	"testing"
+)
+
+func TestCheckAndForeignKeyRejectedOverSimpleQuery(t *testing.T) {
+	c, client := newDataConnection(t)
+	runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte("CREATE TABLE parent (id INTEGER PRIMARY KEY)"), 0)})
+
+	for _, sql := range []string{
+		"CREATE TABLE t1 (id INTEGER PRIMARY KEY, age INTEGER CHECK (age > 0))",
+		"CREATE TABLE t2 (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id))",
+		"CREATE TABLE t3 (id INTEGER PRIMARY KEY, parent_id INTEGER, FOREIGN KEY (parent_id) REFERENCES parent(id))",
+	} {
+		msgs := runQuery(t, c, client, &Message{Type: MsgQuery, Data: append([]byte(sql), 0)})
+		if code := errorCode(msgs[0]); code != ErrCodeFeatureNotSupported {
+			t.Fatalf("%s: error code = %q, want %q", sql, code, ErrCodeFeatureNotSupported)
+		}
+	}
+}