1 
2 
3 module mars.alasql;
4 
5 import std.algorithm;
6 import std.array;
7 import std.conv;
8 import std.format;
9 import std.range;
10 
11 import mars.defs;
12 
13 
14 string insertIntoParameter(const(Table) table)
15 {
16     return "insert into %s values (%s)"
17         .format(table.name, table.columns.map!( (c) => "$" ~ c.name).join(", "));
18 }
19 unittest {
20     auto sql = Table("bar", [Col("foo", Type.text, false), Col("baz", Type.text, false)],[],[]).insertIntoParameter;
21     assert( sql == "insert into bar values ($foo, $baz)", sql );
22 }
23 
24 string updateParameter(const(Table) table)
25 {
26     if( table.pkCols.length >0 ){
27         return "update %s set %s where %s"
28             .format(
29                     table.name,
30                     table.columns.map!( (c) => c.name ~ " = $" ~ c.name).join(", "),
31                     table.pkCols.map!( (c) => c.name ~ " = $key" ~ c.name).join(" AND "),
32                    );
33     }
34     else {
35         return "update %s set %s where %s"
36             .format(
37                     table.name,
38                     table.columns.map!( (c) => c.name ~ " = $" ~ c.name).join(", "),
39                     table.columns.map!( (c) => c.name ~ " = $key" ~ c.name).join(" AND "),
40                    );
41     }
42 }
43 unittest {
44     auto sql = Table("bar", [Col("foo", Type.text, false), Col("baz", Type.text, false)],[0],[]).updateParameter;
45     assert( sql == "update bar set foo = $foo, baz = $baz where foo = $keyfoo", sql );
46 }
47 
48 string deleteFromParameter(const(Table) table)
49 {
50     if( table.pkCols.length >0 ){
51         return "delete from %s where %s"
52             .format(
53                     table.name,
54                     table.pkCols.map!( (c) => c.name ~ " = $" ~ c.name).join(" AND "),
55                    );
56     }
57     else {
58         return "delete from %s where %s"
59             .format(
60                     table.name,
61                     table.columns.map!( (c) => c.name ~ " = $" ~ c.name).join(" AND "),
62                    );
63     }
64 }
65 unittest {
66     auto sql = Table("bar", [Col("foo", Type.text, false), Col("baz", Type.text, false)],[0],[]).deleteFromParameter;
67     assert( sql == "delete from bar where foo = $foo", sql );
68 }
69 
70 string createDatabase(const(Schema) schema)
71 {
72     return schema.tables.map!( t => createTable(schema, t) )().join("; ");
73 }
74 
75 /**
76  * Passing the schema is necessary for being able to refer to foreign table columns names.
77  */
78 string createTable(const(Schema) schema, const(Table) table)
79 {
80         
81     auto primaryKey(T)(T t){
82         ulong i = t.index;
83         string v = t.value;
84 
85         string postfix = "";
86         if( canFind(table.primaryKey, i) ) //  table.primaryKey.length ==1 && table.primaryKey[0] == i )
87             postfix = " primary key";
88         return typeof(t)(i, v ~ postfix);
89     }
90 
91     string references(T)(T t){
92         string r = t.value;
93         ulong i = t.index;
94 
95         foreach(reference; table.references){
96             // ... alasql 0.3.6 does not support references with multiple columns, so skip them.
97             if( reference.referenceCols.length > 1) continue;
98 
99             assert(reference.referenceCols.length == 1); // only one col in alasql references
100             assert(reference.referencedCols.length == 1); // idem
101             if( reference.referenceCols[0] == i ){
102                 r ~= " references " ~ reference.referencedTable ~ "(";
103                 import std.algorithm : find; import std.range : front;
104                 auto tbs = schema.tables.find!( t => t.name == reference.referencedTable );
105                 if( tbs.empty ) assert(false, "table "~ table.name ~ " reference table " ~ reference.referencedTable ~ " that does not exists");
106                 auto tb = tbs.front;
107                 r ~= tb.columns[reference.referencedCols[0]].name ~ ")";
108             }
109         }
110         return r;
111     }
112 
113     string cols = table.columns
114         .map!( (c){ return c.asNameTypeNull; })
115         .ctfeEnumerate
116         //.map!(primaryKey!(EnumerateResult!(ulong, string)))
117         .map!(references!(EnumerateResult!(ulong, string)))
118         .join(", ");
119 
120     string primaryKeys = "";
121     if( table.primaryKey.length > 0 ){
122         primaryKeys = ", primary key (" ~ table.primaryKey.map!( i => table.columns[i].name ).join(", ") ~ ")";
123     }
124 
125     string sql = "create table " ~ table.name ~ " (" ~ cols ~ primaryKeys ~ ")";
126     return sql;
127     
128 }
129 unittest {
130     auto sc = Schema("testschema", [
131         immutable Table("bar1", [ Col("foo", Type.text, false) ], [], [] ),
132         immutable Table("bar2", [Col("foo", Type.text, false), Col("poo", Type.text, false)], [1], []),
133         immutable Table("bar3", [Col("foo", Type.text, false)], [], [Reference([0], "bar2", [1])]),
134         immutable Table("bar4", [Col("foo", Type.text, false), Col("bar", Type.text, false)], [], [Reference([0,1], "bar1", [0,1])]),
135         immutable Table("bar5", [Col("foo", Type.text, false), Col("bar", Type.text, false)], [0, 1], []),
136         ]);
137     
138         string sql = sc.createTable(sc.tables[0]);
139         assert(sql == "create table bar1 (foo text not null)", sql);
140         sql = sc.createTable(sc.tables[1]);
141         assert(sql == "create table bar2 (foo text not null, poo text not null, primary key (poo))", sql);
142         sql = sc.createTable(sc.tables[2]);
143         assert(sql == "create table bar3 (foo text not null references bar2(poo))", sql);
144         sql = sc.createTable(sc.tables[3]);
145         assert(sql == "create table bar4 (foo text not null, bar text not null)", sql);
146         sql = sc.createTable(sc.tables[4]);
147         assert(sql == "create table bar5 (foo text not null, bar text not null, primary key (foo, bar))", sql);
148 }
149 
150 string asNameTypeNull(const(Col) col)
151 {
152     return col.name ~ " " ~ col.type.toSql ~ (col.null_? "" : " not null");
153 }
154 unittest {
155     assert( Col("foo", Type.text, false).asNameTypeNull == "foo text not null" );
156 }
157 
158 /**
159  * See_Also: https://github.com/agershun/alasql/wiki/Data%20Types
160  */
161 string toSql(Type t){
162     import std.conv : to;
163 
164     final switch(t) with(Type) {
165         case boolean: return "boolean";
166         case integer: return "integer";
167         case bigint: return "integer";
168         case smallint: return "smallint"; 
169         case text: return "text";
170         case real_: return "real";
171         case doublePrecision: return "double precision";
172         // ... this is not really supported by alasql, as in javascript we are using 'Buffer', but actually the javascript code
173         //     doesn't perform any check for unknown type, so ...
174         case bytea: return "bytea";
175 
176         // right now handle as a smallint, insert client side not implemented right now
177         case smallserial: return "smallint";
178 
179         case unknown:
180         case date:
181         case serial:
182         case varchar: // varchar(n), tbd as column
183             assert(false, t.to!string); // not implemented right now, catch at CT
184     }
185 }
186 unittest {
187     assert( Type.text.toSql == "text" );
188     assert( Type.integer.toSql == "integer" );
189 }
190 
191 private
192 {
193     // BUG see https://issues.dlang.org/show_bug.cgi?id=15064
194     struct EnumerateResult(I,V) { I index; V value; }
195     struct enumerator(R) {
196         import std.range : ElementType;
197         R r;
198         ulong j = 0;
199         @property EnumerateResult!(ulong, ElementType!R) front() { return EnumerateResult!(ulong, ElementType!R)(j, r.front); }
200         auto empty() { return r.empty; }
201         void popFront() { j++; r.popFront(); }
202     }
203     auto ctfeEnumerate(R)(R r){ return enumerator!R(r); }
204 }