Skip to content

Commit 16e5444

Browse files
committed
Validate required columns
1 parent 447c308 commit 16e5444

4 files changed

Lines changed: 107 additions & 9 deletions

File tree

Sources/Compiler/Sema/StmtTypeChecker.swift

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ extension StmtTypeChecker: StmtSyntaxVisitor {
439439
extension StmtTypeChecker {
440440
mutating func typeCheck(
441441
select: SelectStmtSyntax,
442-
potentialNames: [IdentifierSyntax]? = nil
442+
potentialNames: [Substring]? = nil
443443
) -> ResultColumns {
444444
typeCheck(with: select.with)
445445

@@ -483,7 +483,7 @@ extension StmtTypeChecker {
483483
mutating func typeCheck(
484484
selects: SelectStmtSyntax.Selects,
485485
at location: SourceLocation,
486-
potentialNames: [IdentifierSyntax]? = nil
486+
potentialNames: [Substring]? = nil
487487
) -> ResultColumns {
488488
switch selects {
489489
case let .single(selectCore):
@@ -545,26 +545,55 @@ extension StmtTypeChecker {
545545

546546
usedTableNames.insert(table.name.name)
547547

548+
// If there is an error upstream that will cause unification to fail for sure
549+
// we can just skip it so the real error does not get hidden by all the other errors
550+
var skipInputUnification = false
551+
let columNames: [Substring]
548552
let inputType: Type
549553
if let columns = insert.columns {
550554
var columnTypes: [Type] = []
555+
var setColumns: Set<Substring> = []
551556
for column in columns {
552557
guard let def = table.columns[column.value].first else {
553558
diagnostics.add(.columnDoesNotExist(column))
554559
columnTypes.append(.error)
555560
continue
556561
}
557562

563+
if def.isGenerated {
564+
diagnostics.add(.init("Column is generated and not able to be set", at: column.location))
565+
}
566+
558567
columnTypes.append(def.type)
568+
setColumns.insert(column.value)
569+
}
570+
571+
let requiredColumns = Set(table.requiredColumnsNames)
572+
let missingColumns = requiredColumns.subtracting(setColumns)
573+
574+
if !missingColumns.isEmpty {
575+
let names = missingColumns.map{ "'\($0)'" }.joined(separator: ",")
576+
diagnostics.add(.init("Missing required columns \(names)", at: insert.location))
577+
// Skip unification since it will fail for sure, and this diag is more specific
578+
// to their issue.
579+
skipInputUnification = true
559580
}
581+
560582
inputType = .row(.fixed(columnTypes))
583+
columNames = columns.map(\.value)
561584
} else {
562-
inputType = table.type
585+
let columns = table.nonGeneratedColumns
586+
inputType = .row(.fixed(columns.map(\.1.type)))
587+
columNames = columns.map(\.0)
563588
}
564589

565590
if let values = insert.values {
566-
let resultColumns = typeCheck(select: values.select, potentialNames: insert.columns)
567-
inferenceState.unify(inputType, with: resultColumns.type, at: insert.location)
591+
let resultColumns = typeCheck(select: values.select, potentialNames: columNames)
592+
593+
// Unify the input columns with the actual input values
594+
if !skipInputUnification {
595+
inferenceState.unify(inputType, with: resultColumns.type, at: insert.location)
596+
}
568597
} else {
569598
// TODO: Using 'DEFALUT VALUES' make sure all columns
570599
// TODO: actually have default values or null
@@ -770,7 +799,7 @@ extension StmtTypeChecker {
770799
private mutating func typeCheck(
771800
select: SelectCoreSyntax,
772801
at range: SourceLocation,
773-
potentialNames: [IdentifierSyntax]? = nil
802+
potentialNames: [Substring]? = nil
774803
) -> ResultColumns {
775804
switch select {
776805
case let .select(select):
@@ -788,7 +817,7 @@ extension StmtTypeChecker {
788817
// If there are potential names to match with check to
789818
// see if there is one at the index of this expression.
790819
if let potentialNames, index < potentialNames.count {
791-
nameInferrer.suggest(name: potentialNames[index].value, for: name)
820+
nameInferrer.suggest(name: potentialNames[index], for: name)
792821
}
793822
}
794823

Sources/Compiler/Table.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ public struct Table: Sendable, Equatable {
4040
return .row(.fixed(columns.map(\.value.type)))
4141
}
4242

43+
/// A set of all required columns that need to be set on insert
44+
var nonGeneratedColumns: [(Substring, Column)] {
45+
return columns.reduce(into: []) { result, column in
46+
guard !column.value.isGenerated else { return }
47+
result.append((column.key, column.value))
48+
}
49+
}
50+
51+
/// A set of all required columns that need to be set on insert
52+
var requiredColumnsNames: Set<Substring> {
53+
return columns.reduce(into: []) { result, column in
54+
guard column.value.isRequired else { return }
55+
result.insert(column.key)
56+
}
57+
}
58+
4359
/// A table to be returned incase of an error in type checking
4460
static let error = Table(
4561
name: QualifiedName(name: "<<error>>", schema: nil),

Tests/CompilerTests/Compiler/CompileInsert.sql

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
CREATE TABLE user (id INTEGER, name TEXT);
1+
CREATE TABLE user (
2+
id INTEGER,
3+
name TEXT,
4+
description TEXT GENERATED ALWAYS AS (name || 'is a user')
5+
);
26

37
-- CHECK: SIGNATURE
48
-- CHECK: PARAMETERS
@@ -29,6 +33,7 @@ INSERT INTO user (id, name) VALUES (?, ?);
2933
-- CHECK: OUTPUT
3034
-- CHECK: id INTEGER?
3135
-- CHECK: name TEXT?
36+
-- CHECK: description TEXT?
3237
-- CHECK: TABLES
3338
-- CHECK: user
3439
INSERT INTO user (id, name) VALUES (?, ?) RETURNING *;
@@ -64,6 +69,54 @@ INSERT INTO user (id, name) VALUES (?, ?) RETURNING *;
6469
-- CHECK: OUTPUT
6570
-- CHECK: id INTEGER?
6671
-- CHECK: name TEXT?
72+
-- CHECK: description TEXT?
6773
-- CHECK: TABLES
6874
-- CHECK: user
6975
INSERT INTO user (id, name) VALUES (?, ?), (?, ?), (?, ?) RETURNING *;
76+
77+
-- CHECK: SIGNATURE
78+
-- CHECK: PARAMETERS
79+
-- CHECK: PARAMETER
80+
-- CHECK: TYPE INTEGER?
81+
-- CHECK: INDEX 1
82+
-- CHECK: NAME id
83+
-- CHECK: PARAMETER
84+
-- CHECK: TYPE TEXT?
85+
-- CHECK: INDEX 2
86+
-- CHECK: NAME name
87+
-- CHECK: TABLES
88+
-- CHECK: user
89+
INSERT INTO user VALUES (?, ?);
90+
91+
-- CHECK: SIGNATURE
92+
-- CHECK: PARAMETERS
93+
-- CHECK: PARAMETER
94+
-- CHECK: TYPE INTEGER?
95+
-- CHECK: INDEX 1
96+
-- CHECK: NAME id
97+
-- CHECK: PARAMETER
98+
-- CHECK: TYPE TEXT?
99+
-- CHECK: INDEX 2
100+
-- CHECK: NAME name
101+
-- CHECK: PARAMETER
102+
-- CHECK: TYPE TEXT?
103+
-- CHECK: INDEX 3
104+
-- CHECK: NAME description
105+
-- CHECK: TABLES
106+
-- CHECK: user
107+
-- CHECK-ERROR: Column is generated and not able to be set
108+
INSERT INTO user (id, name, description) VALUES (?, ?, ?);
109+
110+
-- CHECK: SIGNATURE
111+
-- CHECK: PARAMETERS
112+
-- CHECK: PARAMETER
113+
-- CHECK: TYPE TEXT?
114+
-- CHECK: INDEX 1
115+
-- CHECK: NAME name
116+
-- CHECK: PARAMETER
117+
-- CHECK: TYPE INTEGER?
118+
-- CHECK: INDEX 2
119+
-- CHECK: NAME id
120+
-- CHECK: TABLES
121+
-- CHECK: user
122+
INSERT INTO user (name, id) VALUES (?, ?);

Tests/CompilerTests/CompilerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class CompilerTests: XCTestCase {
3131
}
3232

3333
func testTableSchema() throws {
34-
try checkSchema(compile: "CompileTableSchema", dump: true)
34+
try checkSchema(compile: "CompileTableSchema")
3535
}
3636

3737
func testDropTable() throws {

0 commit comments

Comments
 (0)