1 module mars.sqldb;
2 
3 import std.algorithm;
4 import std.array;
5 import std.format;
6 import std.range;
7 
8 import mars.defs;
9 import mars.lexer;
10 
11 class Statement {}
12 class Select : Statement
13 {
14     Col[] cols;
15     immutable(Table)[] tables;
16 
17     string createClientTables() const {
18         return `create table %s (%s)`.format(
19                 tables[0].name,
20                 cols.map!createTableColumn
21                 .joiner(", ")
22                 .array
23                 );
24     }
25 
26     string insertClientRow() const {
27         return `insert into %s (%s) values (%s)`.format(
28                 tables[0].name,
29                 cols.map!name().joiner(", ").array,
30                 cols.map!`"?"`().joiner(", ").array
31                 );
32     }
33 
34     string updateClientWithReturning() const {
35         auto filtered = cols.filter!( (a) => a.type == Type.serial || a.type == Type.smallserial ).array;
36         return `update %s set %s = $%s where %s = $optimistic_%s`.format(
37                 tables[0].name,
38                 filtered[0].name,
39                 filtered[0].name,
40                 filtered[0].name,
41                 filtered[0].name);
42     }
43 
44     string insertServerRow() const {
45         // insert statement, without serial primary keys...
46         auto filtered = cols.filter!( (a) => a.type != Type.serial && a.type != Type.smallserial ).array;
47         auto sql = `insert into quattro_graphql.%s (%s) values (%s)`.format(
48                 tables[0].name,
49                 filtered.map!name().joiner(", ").array,
50                 iota(1, filtered.length+1).map!((a)=> "$%d".format(a))().joiner(", ").array
51                 );
52         auto returning = tables[0].returningCols();
53         if(returning){
54             sql ~= ` returning %s`.format(
55                     returning.map!name().joiner(", ").array
56                     );
57         }
58         return sql;
59     }
60 
61     Col colNamed(string name) const {
62         return cols.filter!((col) => col.name == name)().front;
63     }
64 
65     override string toString() const { 
66         import std.format : format;
67         return "select %s from %s".format(cols, tables);
68     }
69 }
70 class Insert : Statement {
71     immutable(Table)[] tables;
72     Col[] cols;
73     string[] params;
74     Col[] returning;
75 }
76 
77 void fillFrom(ref Col c, immutable Col f){ c.name = f.name; c.type = f.type; c.null_ = f.null_; }
78 string name(const Col col) pure { return col.name; }
79 string createTableColumn(const Col col) pure {
80     enum alasqlTypes = ["unknown", "date", "smallint", "smallint", "int", "text", "varchar"];
81     return `%s %s%s`.format(col.name, alasqlTypes[col.type], (col.null_)? "": " not null") ;
82 }
83 //static assert(createTableColumn(Col("foo", Type.smallint)) == "foo smallint not null");
84 
85 struct Parser 
86 {
87     immutable Schema[] schemas;
88     Token[] tokens;
89     Token t;
90 
91     Statement parse(){
92         advance();
93         if(t.t == TType.select){
94             Select s = new Select;
95             parseCols(s);
96             parseFrom(s);
97             foreach(ref col; s.cols){
98                 auto c = s.tables[0].columns.find!"a.name ==b"(col.name);
99                 if(c.length ==0) throw new Exception("no column named "~col.name~" in table "~s.tables[0].name);
100                 col.fillFrom(c[0]);
101             }
102             return s;
103         }
104         else if(t.t == TType.insert){
105             Insert s = new Insert;
106             advance(); parseInto(s);
107             parseCols(s);
108             parseValues(s);
109             if(tokens.empty) return s;
110             parseReturning(s);
111             return s;
112         }
113 
114         if(!__ctfe){ import std.stdio; writeln(t); }
115         assert(false);
116     }
117 
118     void parseCols(Select s){
119         do {
120             advance();
121             s.cols ~= Col(t.v);
122             advance();
123         } while([TType.comma, TType.dot].canFind(t.t));
124     }
125 
126     void parseCols(Insert s){
127         if(t.t != TType.lparen) throw new Exception("expected left parentesys after 'into'");
128         do {
129             advance();
130             auto col = s.tables[0].columns.filter!((c)=>c.name==t.v)();
131             if(col.empty) throw new Exception("column "~t.v~" does not exists.");
132             s.cols ~= col.front;
133             advance();
134         } while(t.t == TType.comma);
135         if(t.t != TType.rparen) throw new Exception("expected right paren after column list");
136         advance();
137     }
138 
139     void parseInto(Insert s){
140         assert(t.t == TType.into);
141         advance();
142         string schemaName = "public";
143         if( peek.t == TType.dot){
144             schemaName = t.v;
145             advance(); advance();
146         }
147         auto sc = schemas.find!"a.name == b"(schemaName);
148         if(sc.length ==0) throw new Exception("no schema named "~schemaName);
149         auto ta = sc[0].tables.find!"a.name == b"(t.v);
150         if(ta.length ==0) throw new Exception("no table "~t.v~" in schema "~schemaName);
151         s.tables ~= ta[0];
152         advance();
153     }
154 
155     void parseValues(Insert s){
156         if(t.t != TType.values) throw new Exception("expected 'values' after column list");
157         advance();
158         if(t.t != TType.lparen) throw new Exception("expected '(' after 'values'");
159         do {
160             advance();
161             if(t.t == TType.dollar){
162                 advance();
163                 s.params ~= t.v;
164             }
165             else {
166                 throw new Exception("Limitation: insert is supported only via parameter binding");
167             }
168             advance();
169         } while(t.t == TType.comma);
170         if(t.t != TType.rparen) throw new Exception("expected ')' after values list");
171         advance();
172     }
173 
174     void parseReturning(Insert s)
175     {
176         if(t.t != TType.returning) throw new Exception("expected 'returning' after values list");
177         advance();
178         auto col = s.tables[0].columns.filter!((a)=>a.name == t.v)();
179         if(col.empty) throw new Exception("table "~s.tables[0].name~" has no column "~t.v);
180         s.returning ~= col.front;
181         advance();
182     }
183 
184     void parseFrom(Select s){
185         assert(t.t == TType.from);
186         advance();
187         string schemaName = "public";
188         if( peek.t == TType.dot){ 
189             schemaName = t.v;
190             advance(); advance();
191         }
192         auto sc = schemas.find!"a.name == b"(schemaName);
193         if(sc.length ==0) throw new Exception("no schema named "~schemaName~". Available schemas are:"~schemas.map!("a.name")().join(" "));
194         auto ta = sc[0].tables.find!"a.name == b"(t.v);
195         if(ta.length ==0) throw new Exception("no table "~t.v~" in schema "~schemaName);
196         s.tables ~= ta[0];
197     }
198     void advance(){
199         //if(! __ctfe){ import std.stdio; writeln(tokens); }
200         t = tokens[0]; tokens = tokens[1 .. $]; }
201     Token peek() const { return tokens[0]; }
202 }