forked from fsprojects/Rezoom.SQL
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTypeChecker.fs
More file actions
790 lines (752 loc) · 38.3 KB
/
TypeChecker.fs
File metadata and controls
790 lines (752 loc) · 38.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
namespace Rezoom.SQL.Compiler
open System
open System.Collections.Generic
open Rezoom.SQL.Compiler.InferredTypes
type private InferredQueryShape = InferredType QueryExprInfo
[<NoComparison>]
[<NoEquality>]
type private SelfQueryShape =
// this thing is for when we know ahead of time what the column names of a select statement are supposed to be
// so we don't want to require that they all be aliased manually.
{ CTEName : Name option
KnownShape : InferredQueryShape option
}
static member Known(known) = { CTEName = None; KnownShape = known }
static member Known(known) = SelfQueryShape.Known(Some known)
static member Unknown = { CTEName = None; KnownShape = None }
type private TypeChecker(cxt : ITypeInferenceContext, scope : InferredSelectScope) as this =
let exprChecker = ExprTypeChecker(cxt, scope, this)
member this.ObjectName(name) = exprChecker.ObjectName(name)
member this.ObjectName(name, allowNotFound) = exprChecker.ObjectName(name, allowNotFound)
member this.SchemaTableName(name) =
let name = exprChecker.ObjectName(name)
match name.Info with
| TableLike { Table = TableReference _ } -> name
| _ -> failAt name.Source <| Error.objectNotATable name
member this.Expr(expr, knownType) = exprChecker.Expr(expr, knownType)
member this.Expr(expr) = exprChecker.Expr(expr)
member this.BooleanExpr(expr) = this.Expr(expr, BooleanType)
member this.Scope = scope
member this.WithScope(scope) = TypeChecker(cxt, scope)
member private this.TableOrSubqueryScope(tsub : TableOrSubquery) =
match tsub.Table with
| Table tinvoc ->
tsub.Alias |? tinvoc.Table.ObjectName, this.ObjectName(tinvoc.Table).Info
| Subquery select ->
match tsub.Alias with
| None -> failAt select.Source Error.subqueryRequiresAnAlias
| Some alias -> alias, this.Select(select, SelfQueryShape.Unknown).Value.Info
member private this.TableExprScope
(dict : Dictionary<Name, InferredType ObjectInfo>, texpr : TableExpr, outerDepth) =
let add name objectInfo =
if dict.ContainsKey(name) then
failAt texpr.Source <| Error.tableNameAlreadyInScope name
else
dict.Add(name, objectInfo)
match texpr.Value with
| TableOrSubquery tsub ->
let alias, objectInfo = this.TableOrSubqueryScope(tsub)
let objectInfo =
if outerDepth > 0 then
let nullable = NullableDueToJoin |> Seq.replicate outerDepth |> Seq.reduce (>>)
objectInfo.Map(fun t -> { t with InferredNullable = nullable t.InferredNullable })
else objectInfo
add alias objectInfo
outerDepth
| Join join ->
let leftDepth = this.TableExprScope(dict, join.LeftTable, outerDepth)
let depthIncrement = if join.JoinType.IsOuter then 1 else 0
this.TableExprScope(dict, join.RightTable, leftDepth + depthIncrement)
member private this.TableExprScope(texpr : TableExpr) =
let dict = Dictionary()
ignore <| this.TableExprScope(dict, texpr, outerDepth = 0)
{ FromVariables = dict }
member private this.TableOrSubquery(tsub : TableOrSubquery) =
let tbl, info =
match tsub.Table with
| Table tinvoc ->
let invoke = exprChecker.TableInvocation(tinvoc)
Table invoke, invoke.Table.Info
| Subquery select ->
let select = this.Select(select, SelfQueryShape.Unknown)
Subquery select, select.Value.Info
{ Table = tbl
Alias = tsub.Alias
Info = info
}
member private this.TableExpr(constraintChecker : TypeChecker, texpr : TableExpr) =
{ TableExpr.Source = texpr.Source
Value =
match texpr.Value with
| TableOrSubquery tsub -> TableOrSubquery <| this.TableOrSubquery(tsub)
| Join join ->
{ JoinType = join.JoinType
LeftTable = this.TableExpr(constraintChecker, join.LeftTable)
RightTable = this.TableExpr(constraintChecker, join.RightTable)
Constraint =
match join.Constraint with
| JoinOn e -> constraintChecker.Expr(e, BooleanType) |> JoinOn
| JoinUnconstrained -> JoinUnconstrained
} |> Join
}
member this.TableExpr(texpr : TableExpr) =
let checker = TypeChecker(cxt, { scope with FromClause = Some <| this.TableExprScope(texpr) })
checker, this.TableExpr(checker, texpr)
member this.ResultColumn(aliasPrefix : Name option, resultColumn : ResultColumn) =
let qualify (tableAlias : Name) fromTable (col : _ ColumnExprInfo) =
{ Expr.Source = resultColumn.Source
Value =
{ ColumnName = col.ColumnName
Table =
{ Source = resultColumn.Source
ObjectName = tableAlias
SchemaName = None
Info = fromTable
} |> Some
} |> ColumnNameExpr
Info = col.Expr.Info
},
match aliasPrefix with
| None -> None
| Some prefix -> Some (prefix + col.ColumnName)
match resultColumn.Case with
| ColumnsWildcard ->
match scope.FromClause with
| None -> failAt resultColumn.Source Error.wildcardWithoutFromClause
| Some from ->
seq {
for KeyValue(tableAlias, fromTable) in from.FromVariables do
for col in fromTable.Table.Query.Columns do
yield qualify tableAlias fromTable col
}
| TableColumnsWildcard tbl ->
match scope.FromClause with
| None -> failAt resultColumn.Source <| Error.tableWildcardWithoutFromClause tbl
| Some from ->
let succ, fromTable = from.FromVariables.TryGetValue(tbl)
if not succ then failAt resultColumn.Source <| Error.noSuchTableInFrom tbl
fromTable.Table.Query.Columns |> Seq.map (qualify tbl fromTable)
| Column (expr, alias) ->
match aliasPrefix with
| None -> (this.Expr(expr), alias) |> Seq.singleton
| Some prefix ->
let expr = this.Expr(expr)
match implicitAlias (expr.Value, alias) with
| None -> (expr, None) |> Seq.singleton
| Some a -> (expr, Some (prefix + a)) |> Seq.singleton
| ColumnNav nav ->
this.ColumnNav(aliasPrefix, resultColumn, nav)
member this.ColumnNav(aliasPrefix : Name option, resultColumn : ResultColumn, nav : ResultColumnNav<unit, unit>) =
let subAliasPrefix =
let prev =
match aliasPrefix with
| Some prefix -> prefix.Value
| None -> ""
Some <| Name(prev + nav.Name.Value + nav.Cardinality.Separator)
let columns =
seq {
for column in nav.Columns do
let producedColumns = this.ResultColumn(subAliasPrefix, column)
yield column, producedColumns |> ResizeArray
} |> ResizeArray
let keyExprs =
seq {
for source, producedColumns in columns do
match source.Case with
| ColumnNav _ -> () // ignore sub-nav props
| _ ->
for expr, _ in producedColumns do
if expr.Info.PrimaryKey then yield expr
} |> ResizeArray
if keyExprs.Count <= 0 then
failAt resultColumn.Source <| Error.navPropertyMissingKeys nav.Name
else
let minDepthOfImmediateKey =
keyExprs
|> Seq.map (fun e -> e.Info.Type.InferredNullable.JoinInducedNullabilityDepth())
|> Seq.min
columns
|> Seq.collect snd
|> Seq.map (fun (expr, alias) -> // remove nullability introduced by outer joins
{ expr with
Info = { expr.Info with Type = expr.Info.Type.StripNullDueToJoin(minDepthOfImmediateKey) }
}, alias)
member this.ResultColumns(resultColumns : ResultColumns, knownShape : InferredQueryShape option) =
let columns =
resultColumns.Columns
|> Seq.collect
(fun rc ->
this.ResultColumn(None, rc)
|> Seq.map (fun (expr, alias) -> { Source = rc.Source; Case = Column (expr, alias); }))
|> Seq.toArray
match knownShape with
| Some shape ->
if columns.Length <> shape.Columns.Count then
if columns.Length <= 0 then fail "BUG: impossible, parser shouldn't have accepted this"
let source = columns.[columns.Length - 1].Source
failAt source <| Error.expectedKnownColumnCount columns.Length shape.Columns.Count
for i = 0 to columns.Length - 1 do
let selected, alias = columns.[i].Case.AssumeColumn()
let shape = shape.Columns.[i]
cxt.UnifyLeftKnown(selected.Source, shape.Expr.Info.Type, selected.Info.Type)
match implicitAlias (selected.Value, alias) with
| Some a when a = shape.ColumnName -> ()
| _ ->
columns.[i] <- { columns.[i] with Case = Column(selected, Some shape.ColumnName) }
| None ->
for column in columns do
let selected, _ = column.Case.AssumeColumn()
ignore <| cxt.Unify(selected.Source, selected.Info.Type.InferredType, TypeKnown ScalarTypeClass)
{ Distinct = resultColumns.Distinct
Columns = columns
}
member this.GroupBy(groupBy : GroupBy) =
{ By = groupBy.By |> rmap this.Expr
Having = groupBy.Having |> Option.map this.BooleanExpr
}
member this.SelectCore(select : SelectCore, knownShape : InferredQueryShape option) =
let checker, from, staticCount =
match select.From with
| None -> this, None, (if Option.isNone select.Where then Some 1 else None)
| Some from ->
let checker, texpr = this.TableExpr(from)
checker, Some texpr, None
let columns = checker.ResultColumns(select.Columns, knownShape)
let infoColumns =
let used = HashSet()
seq {
for column in columns.Columns do
match column.Case with
| Column (expr, alias) ->
yield
{ Expr = expr
FromAlias = None
ColumnName =
match implicitAlias (expr.Value, alias) with
| None -> failAt column.Source Error.expressionRequiresAlias
| Some alias ->
if used.Add(alias) then alias
else failAt column.Source (Error.ambiguousColumn alias)
}
// typechecker should've eliminated alternatives
| _ -> bug "All wildcards must be expanded -- this is a typechecker bug"
} |> toReadOnlyList
let where, whereIdempotent =
match select.Where with
| None -> None, true
| Some where ->
let where = checker.BooleanExpr(where)
Some where, where.Info.Idempotent
let groupBy, groupByIdempotent =
match select.GroupBy with
| None -> None, true
| Some groupBy ->
let groupBy = checker.GroupBy(groupBy)
let byIdempotent = groupBy.By |> Array.forall (fun e -> e.Info.Idempotent)
let havingIdempotent = groupBy.Having |> Option.forall (fun e -> e.Info.Idempotent)
Some groupBy, byIdempotent && havingIdempotent
checker,
{ Columns = columns
From = from
Where = where
GroupBy = groupBy
Info =
{ Table = SelectResults
Query =
{ Columns = infoColumns
StaticRowCount = staticCount
ClausesIdempotent = whereIdempotent && groupByIdempotent
}
} |> TableLike
} |> AggregateChecker.check
member this.CTE(cte : CommonTableExpression) =
let knownShape = cte.ColumnNames |> Option.map (fun n -> cxt.AnonymousQueryInfo(n.Value))
let select = this.Select(cte.AsSelect, { KnownShape = knownShape; CTEName = Some cte.Name })
{ Name = cte.Name
ColumnNames = cte.ColumnNames
AsSelect = select
Info = select.Value.Info
}
member this.WithClause(withClause : WithClause) =
let mutable scope = scope
let clause =
{ Recursive = withClause.Recursive
Tables =
[| for cte in withClause.Tables ->
let cte = TypeChecker(cxt, scope).CTE(cte)
scope <-
{ scope with
CTEVariables = scope.CTEVariables |> Map.add cte.Name cte.Info.Table.Query
}
cte
|]
}
TypeChecker(cxt, scope), clause
member this.OrderingTerm(orderingTerm : OrderingTerm) =
{ By = this.Expr(orderingTerm.By)
Direction = orderingTerm.Direction
}
member this.Limit(limit : Limit) =
{ Limit = this.Expr(limit.Limit, IntegerType Integer64)
Offset = limit.Offset |> Option.map (fun e -> this.Expr(e, IntegerType Integer64))
}
member this.CompoundTerm(term : CompoundTerm, knownShape : InferredQueryShape option)
: TypeChecker * InfCompoundTerm =
let info, fromChecker, value =
match term.Value, knownShape with
| Values vals, Some shape ->
let vals = vals |> rmap (fun w -> { WithSource.Value = rmap this.Expr w.Value; Source = w.Source })
let columns =
seq {
for rowIndex, row in vals |> Seq.indexed do
if row.Value.Length <> shape.Columns.Count then
failAt row.Source <| Error.expectedKnownColumnCount row.Value.Length shape.Columns.Count
for colVal, colShape in Seq.zip row.Value shape.Columns do
cxt.UnifyLeftKnown(row.Source, colShape.Expr.Info.Type, colVal.Info.Type)
if rowIndex > 0 then () else
yield
{ Expr = colVal
FromAlias = None
ColumnName = colShape.ColumnName
}
} |> toReadOnlyList
let idempotent = vals |> Array.forall (fun r -> r.Value |> Array.forall (fun v -> v.Info.Idempotent))
TableLike
{ Table = CompoundTermResults
Query = { Columns = columns; StaticRowCount = Some vals.Length; ClausesIdempotent = idempotent }
}, this, Values vals
| Values _, None ->
failAt term.Source Error.valuesRequiresKnownShape
| Select select, knownShape ->
let checker, select = this.SelectCore(select, knownShape)
select.Info, checker, Select select
fromChecker, // pass up the typechecker for the "from" clause so "order by" can use it
{ Source = term.Source
Value = value
Info = info
}
member this.Compound(compound : CompoundExpr, knownShape : InferredQueryShape option)
: TypeChecker * InfCompoundExpr =
let nested f leftCompound rightTerm =
match knownShape with
| Some _ ->
let fromChecker, left = this.Compound(leftCompound, knownShape)
let _, right = this.CompoundTerm(rightTerm, knownShape)
fromChecker, f(left, right)
| None ->
let fromChecker, left = this.Compound(leftCompound, None)
let _, right = this.CompoundTerm(rightTerm, Some left.Value.LeftmostInfo.Query)
fromChecker, f(left, right)
let fromChecker, value =
match compound.Value with
| CompoundTerm term ->
let checker, term = this.CompoundTerm(term, knownShape)
checker, CompoundTerm term
| Union (expr, term) -> nested Union expr term
| UnionAll (expr, term) -> nested UnionAll expr term
| Intersect (expr, term) -> nested Intersect expr term
| Except (expr, term) -> nested Except expr term
fromChecker,
{ CompoundExpr.Source = compound.Source
Value = value
}
member this.CompoundTop(compound : CompoundExpr, selfShape : SelfQueryShape)
: TypeChecker * InfCompoundExpr =
match selfShape.CTEName with
| None -> this.Compound(compound, selfShape.KnownShape)
| Some cteName -> // handle recursive references to own CTE in rightmost term
let nested f leftCompound recursiveFinalTerm =
let fromChecker, leftCompound = this.Compound(leftCompound, selfShape.KnownShape)
let leftQuery = leftCompound.Value.LeftmostInfo.Query
let rightChecker =
{ scope with
CTEVariables = scope.CTEVariables |> Map.add cteName leftQuery
} |> this.WithScope
let _, right = rightChecker.CompoundTerm(recursiveFinalTerm, Some leftQuery)
fromChecker, f(leftCompound, right)
let fromChecker, value =
match compound.Value with
| CompoundTerm term ->
let checker, term = this.CompoundTerm(term, selfShape.KnownShape)
checker, CompoundTerm term
| Union (expr, term) -> nested Union expr term
| UnionAll (expr, term) -> nested UnionAll expr term
| Intersect (expr, term) -> nested Intersect expr term
| Except (expr, term) -> nested Except expr term
fromChecker,
{ CompoundExpr.Source = compound.Source
Value = value
}
member this.Select(select : SelectStmt, selfShape : SelfQueryShape) : InfSelectStmt =
{ Source = select.Source
Value =
let select = select.Value
let checker, withClause =
match select.With with
| None -> this, None
| Some withClause ->
let checker, withClause = this.WithClause(withClause)
checker, Some withClause
let fromChecker, compound = checker.CompoundTop(select.Compound, selfShape)
let limit = Option.map checker.Limit select.Limit
let info =
let eitherNull (t1 : InferredType) (t2 : InferredType) =
{ t1 with InferredNullable = InferredNullable.Either(t1.InferredNullable, t2.InferredNullable) }
let merge attemptAdd (leftInfo : InferredType ObjectInfo) (rightInfo : InferredType ObjectInfo) =
match attemptAdd, leftInfo, rightInfo with
| true,
TableLike({ Query = { StaticRowCount = Some left } as lq } as lt),
TableLike { Query = { StaticRowCount = Some right } as rq } ->
let q = lq.MergeInfo(rq, eitherNull)
{ lt with
Query = { q with StaticRowCount = Some (left + right) }
} |> TableLike
| _, TableLike ({ Query = q } as lt), right ->
let q = q.MergeInfo(right.Query, eitherNull)
TableLike { lt with Query = { q with StaticRowCount = None } }
| _ -> bug "Compound expr info must always be table-like!"
match limit, compound.Value.MergeInfo(merge true, merge false) with
| Some _, TableLike ({ Query = { StaticRowCount = Some _ } as query } as table) ->
// if we have any limit expr, drop the static row count
// technically we could figure it out if we're dealing w/ constants, but it's not worth it
TableLike { table with Query = { query with StaticRowCount = None } }
| _, other -> other
let orderBy = Option.map (rmap fromChecker.OrderingTerm) select.OrderBy
let info =
if not info.Query.ClausesIdempotent then info else
match info with
| TableLike t ->
let limitIdem =
limit
|> Option.forall (fun l ->
l.Limit.Info.Idempotent
&& (l.Offset |> Option.forall (fun o -> o.Info.Idempotent)))
let orderByIdem =
orderBy
|> Option.forall (fun o ->
o |> Array.forall (fun e -> e.By.Info.Idempotent))
let idem = limitIdem && orderByIdem
TableLike { t with Query = { t.Query with ClausesIdempotent = idem } }
| other -> other
{ With = withClause
Compound = compound
OrderBy = orderBy
Limit = limit
Info = info
}
}
member this.ForeignKey(foreignKey, creating : CreateTableStmt option) =
let referencesTable, columnNames =
match creating with
| Some tbl when tbl.Name = foreignKey.ReferencesTable -> // self-referencing
this.ObjectName(foreignKey.ReferencesTable, allowNotFound = true),
match tbl.As with
| CreateAsDefinition cdef -> cdef.Columns |> Seq.map (fun c -> c.Value.Name)
| CreateAsSelect _ -> bug "Self-referencing constraints can't exist in a CREATE AS SELECT"
| _ ->
let name = this.ObjectName(foreignKey.ReferencesTable)
name, name.Info.Query.Columns |> Seq.map (fun c -> c.ColumnName)
for { Source = source; Value = referenceName } in foreignKey.ReferencesColumns do
if not (Seq.contains referenceName columnNames) then
failAt source <| Error.noSuchColumn referenceName
{ ReferencesTable = referencesTable
ReferencesColumns = foreignKey.ReferencesColumns
OnDelete = foreignKey.OnDelete
}
member this.ColumnConstraint(constr : ColumnConstraint, creating : CreateTableStmt option) =
{ Name = constr.Name
ColumnConstraintType =
match constr.ColumnConstraintType with
| PrimaryKeyConstraint clause -> PrimaryKeyConstraint clause
| UniqueConstraint -> UniqueConstraint
| ForeignKeyConstraint foreignKey -> ForeignKeyConstraint <| this.ForeignKey(foreignKey, creating)
}
member this.ColumnDef(cdef : ColumnDef WithSource, creating : CreateTableStmt option) =
{ Source = cdef.Source
Value =
let cdef = cdef.Value
{ Name = cdef.Name
Type = cdef.Type
Nullable = cdef.Nullable
Collation = cdef.Collation
DefaultValue = Option.map this.Expr cdef.DefaultValue
Constraints = cdef.Constraints |> rmap (fun con -> this.ColumnConstraint(con, creating))
}
}
member this.Alteration(tableName : InfObjectName, alteration : AlterTableAlteration) =
let inline resolveColumn name =
stateful {
let! qualified = ComplexModelOps.qualify tableName
// IMPROVEMENT source column name?
let! column = ModelOps.getRequiredColumn qualified { Source = tableName.Source; Value = name }
return column
} |> State.runForOuputValue scope.Model
match alteration with
| RenameTo name -> RenameTo name
| AddColumn cdef ->
let hypothetical =
stateful {
let! qualified = ComplexModelOps.qualify tableName
do! ComplexModelOps.addColumnDef qualified cdef
return! ModelOps.getRequiredTable qualified
} |> State.runForOuputValue scope.Model
let from =
InferredFromClause.FromSingleObject
({ tableName with
Info =
{ Table = TableReference hypothetical
Query = inferredOfTable(hypothetical)
} |> TableLike })
let this = this.WithScope({ scope with FromClause = Some from })
AddColumn <| this.ColumnDef(cdef, None)
| AddConstraint constr ->
let this =
match constr.Value.TableConstraintType with
| TableCheckConstraint _ ->
// TODO clean up this code -- but need FROM scope for check expr typechecking!
let hypothetical =
stateful {
let! qualified = ComplexModelOps.qualify tableName
return! ModelOps.getRequiredTable qualified
} |> State.runForOuputValue scope.Model
let from =
InferredFromClause.FromSingleObject
({ tableName with
Info =
{ Table = TableReference hypothetical
Query = inferredOfTable(hypothetical)
} |> TableLike })
this.WithScope({ scope with FromClause = Some from })
| _ -> this
AddConstraint <| this.TableConstraint(constr, None)
| AddDefault (name, expr) -> AddDefault (name, this.Expr(expr))
| DropColumn name -> DropColumn name
| DropConstraint name -> DropConstraint name
| DropDefault name -> DropDefault name
| ChangeType change ->
let schemaColumn = resolveColumn change.Column
{ ExistingInfo = exprInfoOfColumn schemaColumn
Column = change.Column
NewType = change.NewType
} |> ChangeType
| ChangeNullability change ->
let schemaColumn = resolveColumn change.Column
{ ExistingInfo = exprInfoOfColumn schemaColumn
Column = change.Column
NewNullable = change.NewNullable
} |> ChangeNullability
| ChangeCollation change ->
let schemaColumn = resolveColumn change.Column
{ ExistingInfo = exprInfoOfColumn schemaColumn
Column = change.Column
NewCollation = change.NewCollation
} |> ChangeCollation
member this.CreateIndex(createIndex : CreateIndexStmt) =
let tableName = this.SchemaTableName(createIndex.TableName)
let checker =
this.WithScope({ scope with FromClause = Some <| InferredFromClause.FromSingleObject(tableName) })
let query = tableName.Info.Query
for { Source = source; Value = (col, _) } in createIndex.IndexedColumns do
match query.ColumnByName(col) with
| Found _ -> ()
| NotFound _ -> failAt source <| Error.noSuchColumn col
| Ambiguous _ -> failAt source <| Error.ambiguousColumn col
{ Unique = createIndex.Unique
IndexName = this.ObjectName(createIndex.IndexName, allowNotFound = true)
TableName = tableName
IndexedColumns = createIndex.IndexedColumns
Where = createIndex.Where |> Option.map checker.BooleanExpr
}
member this.TableIndexConstraint(constr : TableIndexConstraintClause, creating : CreateTableStmt option) =
match creating with
| Some { As = CreateAsDefinition def } ->
let columnNames = def.Columns |> Seq.map (fun c -> c.Value.Name) |> Set.ofSeq
for { Source = source; Value = (name, _) } in constr.IndexedColumns do
if columnNames |> (not << Set.contains name) then
failAt source <| Error.noSuchColumn name
| _ -> ()
{ Type = constr.Type
IndexedColumns = constr.IndexedColumns
}
member this.TableConstraint(constr : TableConstraint WithSource, creating : CreateTableStmt option) =
{ Source = constr.Source
Value =
let constr = constr.Value
{ Name = constr.Name
TableConstraintType =
match constr.TableConstraintType with
| TableIndexConstraint clause ->
TableIndexConstraint <| this.TableIndexConstraint(clause, creating)
| TableForeignKeyConstraint (names, foreignKey) ->
TableForeignKeyConstraint (names, this.ForeignKey(foreignKey, creating))
| TableCheckConstraint expr -> TableCheckConstraint <| this.Expr(expr)
}
}
member this.CreateTableDefinition
(tableName : InfObjectName, createTable : CreateTableDefinition, creating : CreateTableStmt) =
let hypothetical =
stateful {
let! qualified = ComplexModelOps.qualifyTemp creating.Temporary tableName
do! ComplexModelOps.createTableByDefinition qualified createTable
return! ModelOps.getRequiredTable qualified
} |> State.runForOuputValue scope.Model
let from =
InferredFromClause.FromSingleObject
({ tableName with
Info =
{ Table = TableReference hypothetical
Query = inferredOfTable hypothetical
} |> TableLike })
let this = this.WithScope({ scope with FromClause = Some from })
let creating = Some creating
let columns = createTable.Columns |> rmap (fun col -> this.ColumnDef(col, creating))
{ Columns = columns
Constraints = createTable.Constraints |> rmap (fun con -> this.TableConstraint(con, creating))
}
member this.CreateTable(createTable : CreateTableStmt) =
let name = this.ObjectName(createTable.Name, true)
let name =
match createTable.Temporary, name.SchemaName with
| true, None ->
{ name with SchemaName = Some scope.Model.TemporarySchema }
| _ -> name
{ Temporary = createTable.Temporary
Name = name
As =
match createTable.As with
| CreateAsSelect select -> CreateAsSelect <| this.Select(select, SelfQueryShape.Unknown)
| CreateAsDefinition def -> CreateAsDefinition <| this.CreateTableDefinition(name, def, createTable)
}
member this.CreateView(createView : CreateViewStmt) =
let knownShape = createView.ColumnNames |> Option.map cxt.AnonymousQueryInfo
{ Temporary = createView.Temporary
ViewName = this.ObjectName(createView.ViewName, true)
ColumnNames = createView.ColumnNames
AsSelect = this.Select(createView.AsSelect, SelfQueryShape.Known(knownShape))
}
member this.Delete(delete : DeleteStmt) =
let checker, withClause =
match delete.With with
| None -> this, None
| Some withClause ->
let checker, withClause = this.WithClause(withClause)
checker, Some withClause
let deleteFrom = checker.SchemaTableName(delete.DeleteFrom)
let checker =
checker.WithScope
({ checker.Scope with FromClause = InferredFromClause.FromSingleObject(deleteFrom) |> Some })
{ With = withClause
DeleteFrom = deleteFrom
Where = Option.map checker.BooleanExpr delete.Where
OrderBy = Option.map (rmap checker.OrderingTerm) delete.OrderBy
Limit = Option.map checker.Limit delete.Limit
}
member this.DropObject(drop : DropObjectStmt) =
{ Drop = drop.Drop
ObjectName = this.ObjectName(drop.ObjectName)
}
member this.Insert(insert : InsertStmt) =
let checker, withClause =
match insert.With with
| None -> this, None
| Some withClause ->
let checker, withClause = this.WithClause(withClause)
checker, Some withClause
let table = checker.ObjectName(insert.InsertInto)
let knownShape = table.Info.Query.ColumnsWithNames(insert.Columns)
let columns =
knownShape.Columns
|> Seq.map (fun c -> { WithSource.Source = c.Expr.Source; Value = c.ColumnName })
|> Seq.toArray
match table.Info with
| TableLike { Table = TableReference tbl } ->
let optionalColumns =
let colsWithDefaults =
tbl.Columns
|> Seq.filter (fun c -> c.Value.ColumnType.Nullable || Option.isSome c.Value.DefaultValue)
|> Seq.map (fun c -> c.Value.ColumnName)
|> Set.ofSeq
tbl.Constraints
|> Seq.filter (fun c ->
match c.Value.ConstraintType with
| PrimaryKeyConstraintType true -> true
| _ -> false)
|> Seq.map (fun c -> c.Value.Columns)
|> Seq.fold Set.union colsWithDefaults
let suppliedColumns =
columns
|> Seq.map (fun c -> c.Value)
|> Set.ofSeq
let missingColumns =
tbl.Columns
|> Seq.map (fun c -> c.Key)
|> Seq.filter (fun c -> not (Set.contains c optionalColumns) && not (Set.contains c suppliedColumns))
|> Seq.toArray
if missingColumns.Length > 0 then
failAt insert.Columns.[0].Source (Error.insertMissingColumns missingColumns)
| _ ->
failAt insert.InsertInto.Source Error.insertIntoNonTable
match columns |> tryFindFirstDuplicateBy (fun c -> c.Value) with
| None ->
{ With = withClause
Or = insert.Or
InsertInto = table
Columns = columns // we *must* specify these because our order might not match DB's
Data = checker.Select(insert.Data, SelfQueryShape.Known(knownShape))
}
| Some duplicate ->
failAt duplicate.Source (Error.insertDuplicateColumn duplicate.Value)
member this.Update(update : UpdateStmt) =
let checker, withClause =
match update.With with
| None -> this, None
| Some withClause ->
let checker, withClause = this.WithClause(withClause)
checker, Some withClause
let updateTable = checker.SchemaTableName(update.UpdateTable)
let checker =
checker.WithScope
({ checker.Scope with FromClause = InferredFromClause.FromSingleObject(updateTable) |> Some })
let setColumns =
[| let cols = updateTable.Info.Query
for name, expr in update.Set do
match cols.ColumnByName(name.Value) with
| Found col ->
let expr = checker.Expr(expr)
cxt.UnifyLeftKnown(name.Source, col.Expr.Info.Type, expr.Info.Type)
yield name, expr
| _ ->
failAt name.Source <| Error.noSuchColumnToSet updateTable name.Value
|]
match setColumns |> tryFindFirstDuplicateBy (fun (name, _) -> name.Value) with
| None ->
{ With = withClause
UpdateTable = updateTable
Or = update.Or
Set = setColumns
Where = Option.map checker.Expr update.Where
OrderBy = Option.map (rmap checker.OrderingTerm) update.OrderBy
Limit = Option.map checker.Limit update.Limit
}
| Some (name, _) ->
failAt name.Source (Error.updateDuplicateColumn name.Value)
member this.Stmt(stmt : Stmt) =
match stmt with
| AlterTableStmt alter ->
AlterTableStmt <|
let tbl = this.SchemaTableName(alter.Table)
{ Table = tbl
Alteration = this.Alteration(tbl, alter.Alteration)
}
| CreateIndexStmt index -> CreateIndexStmt <| this.CreateIndex(index)
| CreateTableStmt createTable -> CreateTableStmt <| this.CreateTable(createTable)
| CreateViewStmt createView -> CreateViewStmt <| this.CreateView(createView)
| DeleteStmt delete -> DeleteStmt <| this.Delete(delete)
| DropObjectStmt drop -> DropObjectStmt <| this.DropObject(drop)
| InsertStmt insert -> InsertStmt <| this.Insert(insert)
| SelectStmt select -> SelectStmt <| this.Select(select, SelfQueryShape.Unknown)
| UpdateStmt update -> UpdateStmt <| this.Update(update)
interface IQueryTypeChecker with
member this.Select(select) =
TypeChecker(cxt, scope.Child()).Select(select, SelfQueryShape.Unknown)
member this.CreateView(view) = this.CreateView(view)