├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── identify-term-cruft ├── parse-grammar ├── repl └── walk-grammar ├── project.clj ├── resources ├── dellstore-customers-data.edn ├── dellstore-orders-data.edn ├── dellstore-products-data.edn ├── dellstore-schema.edn ├── seattle-data0.edn ├── seattle-data1.edn ├── seattle-schema.edn ├── sql-92.instaparse.bnf ├── sql-eensy.bnf ├── starfighter-data.edn └── starfighter-schema.edn ├── src ├── data_readers.clj └── sql_datomic │ ├── datomic.clj │ ├── delete_command.clj │ ├── insert_command.clj │ ├── parser.clj │ ├── repl.clj │ ├── retract_command.clj │ ├── schema.clj │ ├── select_command.clj │ ├── tabula.clj │ ├── types.clj │ ├── update_command.clj │ └── util.clj └── test └── sql_datomic ├── integration_test.clj └── parser_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | /bin/hidden 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2016-05-31 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2016-05-31 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/sql-datomic/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/sql-datomic/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2015 NAVIS 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 7 | persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 10 | Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 15 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sql-datomic 2 | 3 | Interpreter of a SQL-ish dialect that runs against Datomic databases. 4 | 5 | Designed to translate basic SQL statements into 6 | legal Datomic-compatible datalog. 7 | 8 | Assumes that the Datomic schema makes use of the `:entity/attribute` 9 | convention for all attributes. 10 | 11 | ## Usage 12 | 13 | To use the interpreter (REPL), run: 14 | ``` 15 | $ bin/repl 16 | ## connects to a default mem db. 17 | ``` 18 | or even: 19 | ``` 20 | $ rlwrap lein run 21 | ``` 22 | To run it against a non-mem Datomic db, run: 23 | ``` 24 | $ bin/repl -u $datomic_connect_uri 25 | ## same kind of URI you would hand to datomic.api/connect. 26 | ``` 27 | 28 | To run the tests, run: 29 | ``` 30 | $ lein test 31 | ``` 32 | 33 | For a list of command-line flags available to the interpreter, run: 34 | ``` 35 | $ bin/repl --help 36 | ``` 37 | or equivalently: 38 | ``` 39 | $ lein run -- --help 40 | ``` 41 | 42 | For a list of supported in-interpreter commands, run: 43 | ``` 44 | sql> help 45 | ``` 46 | from within a running interpreter. 47 | 48 | ## Sample session 49 | 50 | ``` 51 | sql> \d 52 | sql> \dn 53 | sql> \x 54 | sql> select where product.prod-id = 9990 55 | sql> \x 56 | sql> debug 57 | sql> select where order.orderdate between #inst "2004-01-01" and #inst "2004-01-05" 58 | sql> select product.prod-id, #attr :product/tag, product.title where product.prod-id = 9990 59 | sql> select db.id, customer.city, customer.state, customer.zip from customer where customer.customerid = 4858 60 | sql> debug 61 | sql> update customer set customer.city = 'Springfield', customer.state = 'VA', customer.zip = '22150' where customer.customerid = 4858 62 | sql> select db.id, customer.city, customer.state, customer.zip from customer where customer.customerid = 4858 63 | sql> \x 64 | sql> select where product.prod-id = 9999 65 | sql> insert into #attr :product/prod-id = 9999, #attr :product/actor = 'Naomi Watts', #attr :product/title = 'The Ring', product.category = :product.category/horror, product.rating = 4.5f, product.man-hours = 9001N, product.price = 21.99M 66 | sql> select where product.prod-id = 9999 67 | sql> delete from product where product.prod-id = 9999 68 | sql> select where product.prod-id = 9999 69 | sql> \d order 70 | sql> \d orderline 71 | sql> select where order.orderid > 0 72 | sql> select where orderline.orderlineid > 0 73 | sql> delete from order where order.orderid > 0 74 | sql> \d order 75 | sql> \d orderline 76 | sql> select where order.orderid > 0 77 | sql> select where orderline.orderlineid > 0 78 | sql> \x 79 | sql> \d customer 80 | sql> select db.id, #attr :customer/customerid, #attr :customer/username, #attr :customer/password from customer where customer.customerid > 0 81 | sql> pretend 82 | sql> update customer set customer.username = 'donald.duck', customer.password = 'somethingclever' where customer.customerid = 14771 83 | sql> pretend 84 | sql> debug 85 | sql> select db.id, #attr :customer/customerid, #attr :customer/username, #attr :customer/password from customer where customer.customerid > 0 86 | sql> update customer set customer.username = 'donald.duck', customer.password = 'somethingclever' where customer.customerid = 14771 87 | sql> select db.id, #attr :customer/customerid, #attr :customer/username, #attr :customer/password from customer where customer.customerid > 0 88 | ``` 89 | 90 | ## Differences from standard SQL 91 | 92 | The SQL used by this tool is largely a subset of ANSI SQL-92, with the 93 | following deviations: 94 | 95 | - Accepts only `SELECT`, `INSERT`, `UPDATE`, `DELETE` statements. 96 | - Adds the notion of a `RETRACT` statement as a way to drop a field 97 | from a given "row" (really, entity). Affects that entity alone; does 98 | not affect schema for other rows / entities: 99 | `RETRACT actor.realname WHERE db.id = 123245` 100 | (In Datomic parlance, this retracts that E-A-V fact; V is gathered 101 | automatically on the user's behalf. For more on retraction, 102 | [please refer to the documentation](http://docs.datomic.com/transactions.html#retracting-data).) 103 | - Column names must be fully qualified (`table_name.column_name`) 104 | for `select`, `update` and `delete`; `insert` are exempt from this. 105 | - Raw attribute keywords may be used in place of column names: 106 | (e.g., using `#attr :product/title` directly instead of 107 | `product.title`). 108 | - No support for explicit `JOIN`s. Instead, use implicit joins 109 | (which is more like Datomic's syntax anyhow). 110 | - No support for `NULL`, `IS NULL`, `IS NOT NULL` (no meaning in Datomic). 111 | - No support for general arithmetic operators, string operators 112 | or any functions. 113 | - Hyphens are permitted in table names and column names: 114 | `survey-request.sent-date`. (Helps with Datomic interop.) 115 | - ~~Aliases are permitted on table names:~~ 116 | - ~~`select "la la la".foo from bar "la la la"`~~ 117 | - ~~`select lalala.foo from bar as lalala`~~ 118 | - `FROM` clauses must consist of table names only (no subselects). 119 | - `WHERE` clauses support `AND`, `OR`, and `NOT` terms. 120 | - `IN` clauses are supported in `WHERE`s. 121 | - `BETWEEN` clauses must operate on a column, and use either 122 | numeric literals or chronological literals (`DATE`, `DATETIME`, `TIME`) 123 | for bounds. 124 | - Supported scalar types are: 125 | - boolean: `TRUE`, `FALSE` (`true`, `false`) 126 | - numeric (int, float): `42`, `3.14159`, `6.62607004e-34` 127 | - int-types: 128 | - long: `42` 129 | - bigint: `9001N` 130 | - float-types: 131 | - double: `3.14159` 132 | - float: `2.7182F`, `1.6182f`, `#float -5.24e-5` 133 | - bigdec: `24.95M` (often used for monetary values) 134 | - strings: `''`, `'foo'`, `'bIlujDI\\' yIchegh()Qo\\'; yIHegh()!'` 135 | - chronological literals (all assumed to be UTC): 136 | - `date '1999-12-31'` 137 | - `datetime '1970-04-01T09:01:00'` 138 | - `time '1465423112'` 139 | - Supported comparison operators: 140 | - `=`, `<>`, `!=` for columns and all scalars 141 | - `<`, `<=`, `>`, `>=` for columns, numeric and chronological types 142 | 143 | ### Datomic-native data types supported in SQL dialect 144 | 145 | In addition to the usual data types expected in a database 146 | (e.g., string, ints, floats, booleans), Datomic also supports some 147 | interesting additional data types. To better support them at the 148 | SQL prompt, the following are supported (most with Tagged Literals): 149 | 150 | - UUID: `#uuid "5760745a-5bb5-4768-96f7-0f8aeb1a84f0"` 151 | - URI: `#uri "http://slashdot.org/"` 152 | - Byte Array (base-64): `#bytes "b2hhaQ=="` 153 | - Instant (like datetimes): `#inst "1987-01-14T10:30:00"` 154 | - Keyword: `:foo.bar/baz-quux`, `:ohai` 155 | - Float: `#float 3.1415` 156 | 157 | Note that byte arrays are not value types (from Datomic's perspective); 158 | therefore, they are not supported in `WHERE` clauses. 159 | 160 | For more about Datomic native types, please 161 | [read the fine documentation](http://docs.datomic.com/schema.html#text-1-1). 162 | 163 | For more about Tagged Literals, please 164 | [have a look at this](http://clojure.org/reference/reader#_tagged_literals). 165 | 166 | ## Shortened convenience forms 167 | 168 | - select: 169 | - `select where #attr :product/prod-id between 1567 and 6000` 170 | - insert: 171 | - `insert 172 | #attr :product/prod-id = 1984, 173 | #attr :product/actor = 'Quux', 174 | #attr :product/title = 'Foo Bar', 175 | #attr :product/price = 21.99M` 176 | - delete: 177 | - `delete where #attr :product/prod-id between 1567 and 6000` 178 | - update: 179 | - `update product.rating = 3.5f where product.prod-id = 1567` 180 | 181 | ## Currently supported command-line flags: 182 | 183 | ``` 184 | Switches Default Desc 185 | -------- ------- ---- 186 | -h, --no-help, --help false Print this help 187 | -d, --no-debug, --debug false Write debug info to stderr 188 | -p, --no-pretend, --pretend false Run without transacting; turns on debug 189 | -x, --no-expanded, --expanded false Display resultsets in expanded output format 190 | -u, --connection-uri URI to Datomic DB; if missing, uses default mem db 191 | -s, --default-schema-name :dellstore :dellstore or :starfighter, for default in-mem db 192 | ``` 193 | 194 | ## Currently supported interpreter commands: 195 | 196 | ``` 197 | type `exit` or `quit` or ^D to exit 198 | type `debug` to toggle debug mode 199 | type `pretend` to toggle pretend mode 200 | type `expanded` or `\x` to toggle expanded display mode 201 | type `show tables` or `\d` to show Datomic "tables" 202 | type `show schema` or `\dn` to show all user Datomic schema 203 | type `describe $table` or `\d $table` to describe a Datomic "table" 204 | type `describe $dbid` or `\d $dbid` to describe the schema of an entity 205 | type `status` to show toggle values, conn strings, etc. 206 | type `\?`, `?`, `h` or `help` to see this listing 207 | ``` 208 | 209 | ## Running the test suite 210 | 211 | ``` 212 | $ lein test 213 | ``` 214 | 215 | ## Caveats 216 | 217 | This is a rather sharp tool (although the `pretend` flag helps). It will carry out 218 | all mutating statements immediately (i.e., behaves as if run in an AUTOCOMMIT mode). 219 | Again, judicious use of `pretend` can help immensely. 220 | 221 | ## TODO 222 | 223 | - Finish support for aliases on table names; they parse correctly, but 224 | the query engine does not yet understand them. 225 | - Support some kind of multiline statement input from within the 226 | interpreter; the parser and the query engine have no trouble with 227 | multiline input (see the tests). 228 | 229 | ## License 230 | 231 | The MIT License (MIT) Copyright © 2016 NAVIS 232 | -------------------------------------------------------------------------------- /bin/identify-term-cruft: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | n="${1:-1}" 5 | 6 | sed -n '2,$p' resources/sql-92.instaparse.bnf \ 7 | |perl -ple 's{(?:\+|\*)}{}g' \ 8 | |perl -nle 'my @s = split /\s+/, $_; print for grep { /^[a-z0-9_]+$/ } @s' \ 9 | |sort \ 10 | |uniq -c \ 11 | |n="$n" perl -le ' 12 | my $n = $ENV{n}; 13 | my $re = qr/^\s+$n\s/o; 14 | while (<>) { chomp; print if /$re/ }' 15 | -------------------------------------------------------------------------------- /bin/parse-grammar: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use JSON qw( to_json ); 5 | 6 | my $grammar = do { local $/; <> }; 7 | 8 | my @productions = grep { /\S/xms } 9 | grep { !/^\s*\(\*/xms } 10 | split(/(\n|\r\n){2,}/xms, $grammar, -1); 11 | 12 | my %graph; 13 | for my $p (@productions) { 14 | my ($t, $r) = split_nonterm($p); 15 | my @nts = gather_rule_nonterms($r); 16 | $graph{$t} = { to => \@nts, text => $p }; 17 | } 18 | print to_json(\%graph, { pretty => 1 }), "\n"; 19 | 20 | exit; 21 | 22 | sub split_nonterm { 23 | my ($production) = @_; 24 | my ($nonterm, $rule) = split(/\s*::=\s*/xms, $production, 2); 25 | return ($nonterm, $rule); 26 | } 27 | 28 | sub gather_rule_nonterms { 29 | my ($rule) = @_; 30 | for ($rule) { 31 | s{#'[^']+'}{}msg; 32 | s{#"[^"]+"}{}msg; 33 | s{'[^']+'}{}xmsg; 34 | s{"[^"]+"}{}xmsg; 35 | } 36 | my @nts = $rule =~ /([-\w]+|<[-\w]+>)/xmsg; 37 | my %uni = map { $_ => 1 } @nts; 38 | return sort { $a cmp $b } keys(%uni); 39 | } 40 | -------------------------------------------------------------------------------- /bin/repl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | cd "$(dirname $0)/.." 5 | 6 | # Uses rlwrap if available, but will still run otherwise. 7 | exec $(which rlwrap) lein run -- "$@" 8 | -------------------------------------------------------------------------------- /bin/walk-grammar: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | use strict; 3 | use warnings; 4 | use JSON qw( from_json ); 5 | 6 | my $start = $ENV{start} || 'direct_SQL_data_statement'; 7 | 8 | my $json_text = do { local $/; <> }; 9 | my $grammar = from_json($json_text); 10 | 11 | # breadth-first walk 12 | my %seen; 13 | my @todo = ($start); 14 | while (@todo) { 15 | my $term = shift(@todo); 16 | next if $seen{$term}; 17 | my $node = $grammar->{$term}; 18 | die "unknown term: $term\n" unless $node; 19 | $seen{$term} = 1; 20 | my $text = $node->{text}; 21 | print $text, "\n\n"; 22 | my @nexties = @{$node->{to}}; 23 | push @todo, @nexties if @nexties; 24 | } 25 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject sql-datomic "0.1.0" 2 | :description "Interpreter of a SQL-ish dialect that runs against Datomic databases." 3 | :url "https://github.com/untangled-web/sql-datomic" 4 | :license {:name "The MIT License" 5 | :url "http://opensource.org/licenses/MIT"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [instaparse "1.4.2"] 8 | [clj-time "0.12.0"] 9 | #_[com.datomic/datomic-pro "0.9.5206" :exclusions [joda-time]] 10 | [com.datomic/datomic-free "0.9.5344" :scope "provided" :exclusions [joda-time]] 11 | [com.stuartsierra/component "0.3.1"] 12 | [org.clojure/tools.cli "0.3.5"] 13 | [org.postgresql/postgresql "9.4.1208.jre7"] 14 | [com.datastax.cassandra/cassandra-driver-core "2.0.6" 15 | :exclusions [com.google.guava/guava 16 | org.slf4j/slf4j-api]]] 17 | :main sql-datomic.repl 18 | ;; :aot [sql-datomic.repl] 19 | ) 20 | -------------------------------------------------------------------------------- /resources/dellstore-customers-data.edn: -------------------------------------------------------------------------------- 1 | [ 2 | ;; orderid 2 3 | {:db/id #db/id[:db.part/user] 4 | :customer/creditcard "9752442379947752" 5 | :customer/income 20000M 6 | :customer/email "OVWOIYIDDL@dell.com" 7 | :customer/lastname "OVWOIYIDDL" 8 | :customer/phone "6298730047" 9 | :customer/password "password" 10 | :customer/age 56 11 | :customer/creditcardexpiration "2010/04" 12 | :customer/state "IA" 13 | :customer/city "IQIDDJY" 14 | :customer/creditcardtype 5 15 | :customer/zip "98082" 16 | :customer/username "user4858" 17 | :customer/address-1 "6298730047 Dell Way" 18 | :customer/region 1 19 | :customer/firstname "SKULRB" 20 | :customer/customerid 4858 21 | :customer/country "US" 22 | :customer/gender :customer.gender/male} 23 | 24 | ;; orderid 5 25 | {:customer/creditcard "2564112400636077" 26 | :customer/income 40000M 27 | :customer/email "JSCBIDJQER@dell.com" 28 | :customer/lastname "JSCBIDJQER" 29 | :customer/phone "6447516578" 30 | :customer/password "password" 31 | :customer/age 44 32 | :customer/creditcardexpiration "2009/10" 33 | :customer/city "KEFQJES" 34 | :customer/creditcardtype 3 35 | :customer/zip "0" 36 | :customer/username "user14771" 37 | :customer/address-1 "6447516578 Dell Way" 38 | :customer/region 2 39 | :customer/firstname "JIDXLK" 40 | :db/id #db/id[:db.part/user] 41 | :customer/customerid 14771 42 | :customer/country "Canada" 43 | :customer/gender :customer.gender/female}] 44 | -------------------------------------------------------------------------------- /resources/dellstore-orders-data.edn: -------------------------------------------------------------------------------- 1 | [{:db/id #db/id[:db.part/user] 2 | :order/orderid 2 3 | :order/tax 4.53M 4 | :order/netamount 54.90M 5 | :order/customerid 4858 6 | :order/customer [:customer/customerid 4858] 7 | :order/orderlines 8 | #{{:db/id #db/id[:db.part/user] 9 | :orderline/orderlineid 6 10 | :orderline/orderid 2 11 | :orderline/prod-id 6376 12 | :orderline/product [:product/prod-id 6376] 13 | :orderline/quantity 2 14 | :orderline/orderdate #inst "2004-01-01T08:00:00.000-00:00"} 15 | {:db/id #db/id[:db.part/user] 16 | :orderline/orderlineid 5 17 | :orderline/orderid 2 18 | :orderline/prod-id 6127 19 | :orderline/product [:product/prod-id 6127] 20 | :orderline/quantity 1 21 | :orderline/orderdate #inst "2004-01-01T08:00:00.000-00:00"} 22 | {:db/id #db/id[:db.part/user] 23 | :orderline/orderlineid 3 24 | :orderline/orderid 2 25 | :orderline/prod-id 9990 26 | :orderline/product [:product/prod-id 9990] 27 | :orderline/quantity 1 28 | :orderline/orderdate #inst "2004-01-01T08:00:00.000-00:00"} 29 | {:db/id #db/id[:db.part/user] 30 | :orderline/orderlineid 4 31 | :orderline/orderid 2 32 | :orderline/prod-id 5130 33 | :orderline/product [:product/prod-id 5130] 34 | :orderline/quantity 3 35 | :orderline/orderdate #inst "2004-01-01T08:00:00.000-00:00"} 36 | {:db/id #db/id[:db.part/user] 37 | :orderline/orderlineid 1 38 | :orderline/orderid 2 39 | :orderline/prod-id 1567 40 | :orderline/product [:product/prod-id 1567] 41 | :orderline/quantity 2 42 | :orderline/orderdate #inst "2004-01-01T08:00:00.000-00:00"} 43 | {:db/id #db/id[:db.part/user] 44 | :orderline/orderlineid 2 45 | :orderline/orderid 2 46 | :orderline/prod-id 1298 47 | :orderline/product [:product/prod-id 1298] 48 | :orderline/quantity 1 49 | :orderline/orderdate #inst "2004-01-01T08:00:00.000-00:00"} 50 | {:db/id #db/id[:db.part/user] 51 | :orderline/orderlineid 8 52 | :orderline/orderid 2 53 | :orderline/prod-id 2926 54 | :orderline/product [:product/prod-id 2926] 55 | :orderline/quantity 3 56 | :orderline/orderdate #inst "2004-01-01T08:00:00.000-00:00"} 57 | {:db/id #db/id[:db.part/user] 58 | :orderline/orderlineid 7 59 | :orderline/orderid 2 60 | :orderline/prod-id 4936 61 | :orderline/product [:product/prod-id 4936] 62 | :orderline/quantity 3 63 | :orderline/orderdate #inst "2004-01-01T08:00:00.000-00:00"}} 64 | :order/totalamount 59.43M 65 | :order/orderdate #inst "2004-01-01T08:00:00.000-00:00"} 66 | 67 | {:db/id #db/id[:db.part/user] 68 | :order/orderid 5 69 | :order/tax 21.12M 70 | :order/netamount 256.00M 71 | :order/customerid 14771 72 | :order/customer [:customer/customerid 14771] 73 | :order/orderlines 74 | #{{:db/id #db/id[:db.part/user] 75 | :orderline/orderlineid 3 76 | :orderline/orderid 5 77 | :orderline/prod-id 8293 78 | :orderline/product [:product/prod-id 8293] 79 | :orderline/quantity 1 80 | :orderline/orderdate #inst "2004-01-09T08:00:00.000-00:00"} 81 | {:db/id #db/id[:db.part/user] 82 | :orderline/orderlineid 2 83 | :orderline/orderid 5 84 | :orderline/prod-id 4402 85 | :orderline/product [:product/prod-id 4402] 86 | :orderline/quantity 3 87 | :orderline/orderdate #inst "2004-01-09T08:00:00.000-00:00"} 88 | {:db/id #db/id[:db.part/user] 89 | :orderline/orderlineid 4 90 | :orderline/orderid 5 91 | :orderline/prod-id 2290 92 | :orderline/product [:product/prod-id 2290] 93 | :orderline/quantity 3 94 | :orderline/orderdate #inst "2004-01-09T08:00:00.000-00:00"} 95 | {:db/id #db/id[:db.part/user] 96 | :orderline/orderlineid 1 97 | :orderline/orderid 5 98 | :orderline/prod-id 6879 99 | :orderline/product [:product/prod-id 6879] 100 | :orderline/quantity 1 101 | :orderline/orderdate #inst "2004-01-09T08:00:00.000-00:00"}} 102 | :order/totalamount 277.12M 103 | :order/orderdate #inst "2004-01-09T08:00:00.000-00:00"} 104 | ] 105 | -------------------------------------------------------------------------------- /resources/dellstore-products-data.edn: -------------------------------------------------------------------------------- 1 | [ 2 | ;; orderid 2 3 | {:db/id #db/id[:db.part/user] 4 | :product/prod-id 6127 5 | :product/url #uri "http://example.com/products/6127" 6 | :product/category :product.category/games 7 | :product/title "AIRPLANE CAT" 8 | :product/actor "RIP DOUGLAS" 9 | :product/price 16.99M 10 | :product/special false 11 | :product/common-prod-id 1644 12 | :product/tag :airplane-cat-games 13 | :product/uuid #uuid "57607364-9b35-4515-b067-88fe80de59a3" 14 | :product/blob #bytes "QUlSUExBTkUgQ0FU" 15 | :product/rating #float 1.0 16 | :product/man-hours 10100200300N} 17 | {:db/id #db/id[:db.part/user] 18 | :product/prod-id 4936 19 | :product/url #uri "http://example.com/products/4936" 20 | :product/category :product.category/new 21 | :product/title "AFRICAN VANISHING" 22 | :product/actor "EDWARD NOLTE" 23 | :product/price 16.99M 24 | :product/special false 25 | :product/common-prod-id 4633 26 | :product/tag :african-vanishing-new 27 | :product/uuid #uuid "5760739f-b352-4a41-82b9-98f6819e28cc" 28 | :product/blob #bytes "QUZSSUNBTiBWQU5JU0hJTkc=" 29 | :product/rating #float 1.2 30 | :product/man-hours 20100200300N} 31 | {:db/id #db/id[:db.part/user] 32 | :product/prod-id 1298 33 | :product/url #uri "http://example.com/products/1298" 34 | :product/category :product.category/comedy 35 | :product/title "ACE EYES" 36 | :product/actor "DENNIS FIENNES" 37 | :product/price 26.99M 38 | :product/special false 39 | :product/common-prod-id 5006 40 | :product/tag :ace-eyes-comedy 41 | :product/uuid #uuid "576073c3-24c5-4461-9b84-dfe65774d41b" 42 | :product/blob #bytes "QUNFIEVZRVM=" 43 | :product/rating #float 1.4 44 | :product/man-hours 30100200300N} 45 | {:db/id #db/id[:db.part/user] 46 | :product/prod-id 2926 47 | :product/url #uri "http://example.com/products/2926" 48 | :product/category :product.category/music 49 | :product/title "ADAPTATION UNTOUCHABLES" 50 | :product/actor "HELEN BERRY" 51 | :product/price 22.99M 52 | :product/special false 53 | :product/common-prod-id 2561 54 | :product/tag :adaptation-untouchables-music 55 | :product/uuid #uuid "576073e7-d671-45ba-af1a-f08a9a355b81" 56 | :product/blob #bytes "QURBUFRBVElPTiBVTlRPVUNIQUJMRVM=" 57 | :product/rating #float 1.6 58 | :product/man-hours 40100200300N} 59 | {:db/id #db/id[:db.part/user] 60 | :product/prod-id 5130 61 | :product/url #uri "http://example.com/products/5130" 62 | :product/category :product.category/action 63 | :product/title "AGENT CELEBRITY" 64 | :product/actor "ORLANDO KILMER" 65 | :product/price 14.99M 66 | :product/special false 67 | :product/common-prod-id 8074 68 | :product/tag :agent-celebrity-action 69 | :product/uuid #uuid "5760740c-4f24-4f3d-8455-f79a1cc57fa9" 70 | :product/blob #bytes "QUdFTlQgQ0VMRUJSSVRZ" 71 | :product/rating #float 1.8 72 | :product/man-hours 50100200300N} 73 | {:db/id #db/id[:db.part/user] 74 | :product/prod-id 9990 75 | :product/url #uri "http://example.com/products/9990" 76 | :product/category :product.category/music 77 | :product/title "ALADDIN WORLD" 78 | :product/actor "HUMPHREY DENCH" 79 | :product/price 25.99M 80 | :product/special false 81 | :product/common-prod-id 6584 82 | :product/tag :aladdin-world-music 83 | :product/uuid #uuid "57607426-cdd4-49fa-aecb-0a2572976db9" 84 | :product/blob #bytes "QUxBRERJTiBXT1JMRA==" 85 | :product/rating #float 2.0 86 | :product/man-hours 60100200300N} 87 | {:db/id #db/id[:db.part/user] 88 | :product/prod-id 1567 89 | :product/url #uri "http://example.com/products/1567" 90 | :product/category :product.category/new 91 | :product/title "ACE MEET" 92 | :product/actor "JESSICA PFEIFFER" 93 | :product/price 25.99M 94 | :product/special false 95 | :product/common-prod-id 5179 96 | :product/tag :ace-meet-new 97 | :product/uuid #uuid "5760743c-33b3-4d8c-8f65-7ef389be597c" 98 | :product/blob #bytes "QUNFIE1FRVQ=" 99 | :product/rating #float 2.2 100 | :product/man-hours 70100200300N} 101 | {:db/id #db/id[:db.part/user] 102 | :product/prod-id 6376 103 | :product/url #uri "http://example.com/products/6376" 104 | :product/category :product.category/drama 105 | :product/title "AIRPLANE GRAPES" 106 | :product/actor "KIM RYDER" 107 | :product/price 21.99M 108 | :product/special false 109 | :product/common-prod-id 7411 110 | :product/tag :airplane-grapes-drama 111 | :product/uuid #uuid "5760745a-5bb5-4768-96f7-0f8aeb1a84f0" 112 | :product/blob #bytes "QUlSUExBTkUgR1JBUEVT" 113 | :product/rating #float 2.4 114 | :product/man-hours 80100200300N} 115 | 116 | ;; orderid 5 117 | {:db/id #db/id[:db.part/user] 118 | :product/prod-id 8293 119 | :product/url #uri "http://example.com/products/8293" 120 | :product/category :product.category/family 121 | :product/title "ALABAMA EXORCIST" 122 | :product/actor "NICK MINELLI" 123 | :product/price 13.99M 124 | :product/special false 125 | :product/common-prod-id 4804 126 | :product/tag :alabama-exorcist-family 127 | :product/uuid #uuid "57607472-0bd8-4ed3-98d3-586e9e9c9683" 128 | :product/blob #bytes "QUxBQkFNQSBFWE9SQ0lTVA==" 129 | :product/rating #float 2.6 130 | :product/man-hours 100N} 131 | {:db/id #db/id[:db.part/user] 132 | :product/prod-id 4402 133 | :product/url #uri "http://example.com/products/4402" 134 | :product/category :product.category/children 135 | :product/title "AFRICAN HARPER" 136 | :product/actor "GENE WILLIS" 137 | :product/price 11.99M 138 | :product/special false 139 | :product/common-prod-id 1261 140 | :product/tag :african-harper-children 141 | :product/uuid #uuid "5760748b-7898-499c-af26-871426422d08" 142 | :product/blob #bytes "QUZSSUNBTiBIQVJQRVI=" 143 | :product/rating #float 2.8 144 | :product/man-hours 101N} 145 | {:db/id #db/id[:db.part/user] 146 | :product/prod-id 6879 147 | :product/url #uri "http://example.com/products/6879" 148 | :product/category :product.category/action 149 | :product/title "AIRPLANE TELEGRAPH" 150 | :product/actor "LIAM KILMER" 151 | :product/price 12.99M 152 | :product/special false 153 | :product/common-prod-id 6893 154 | :product/tag :airplane-telegraph-action 155 | :product/uuid #uuid "576074a9-8d11-4468-bb46-bebcc23646fa" 156 | :product/blob #bytes "QUlSUExBTkUgVEVMRUdSQVBI" 157 | :product/rating #float 3.0 158 | :product/man-hours 102N} 159 | {:db/id #db/id[:db.part/user] 160 | :product/prod-id 2290 161 | :product/url #uri "http://example.com/products/2290" 162 | :product/category :product.category/documentary 163 | :product/title "ADAPTATION EVERYONE" 164 | :product/actor "ANNETTE BEATTY" 165 | :product/price 15.99M 166 | :product/special false 167 | :product/common-prod-id 2928 168 | :product/tag :adaptation-everyone-documentary 169 | :product/uuid #uuid "576074cd-9f64-42cf-b604-4dee95de1303" 170 | :product/blob #bytes "QURBUFRBVElPTiBFVkVSWU9ORQ==" 171 | :product/rating #float 3.2 172 | :product/man-hours 103N}] 173 | -------------------------------------------------------------------------------- /resources/dellstore-schema.edn: -------------------------------------------------------------------------------- 1 | [ 2 | 3 | ;; dellstore2=# \d+ orders 4 | ;; Table "public.orders" 5 | ;; Column | Type | Modifiers | Storage | Stats target | Description 6 | ;; -------------+---------------+----------------------------------------------------------+---------+--------------+------------- 7 | ;; orderid | integer | not null default nextval('orders_orderid_seq'::regclass) | plain | | 8 | ;; orderdate | date | not null | plain | | 9 | ;; customerid | integer | | plain | | 10 | ;; netamount | numeric(12,2) | not null | main | | 11 | ;; tax | numeric(12,2) | not null | main | | 12 | ;; totalamount | numeric(12,2) | not null | main | | 13 | ;; Indexes: 14 | ;; "orders_pkey" PRIMARY KEY, btree (orderid) 15 | ;; "ix_order_custid" btree (customerid) 16 | ;; Foreign-key constraints: 17 | ;; "fk_customerid" FOREIGN KEY (customerid) REFERENCES customers(customerid) ON DELETE SET NULL 18 | ;; Referenced by: 19 | ;; TABLE "orderlines" CONSTRAINT "fk_orderid" FOREIGN KEY (orderid) REFERENCES orders(orderid) ON DELETE CASCADE 20 | 21 | {:db/id #db/id[:db.part/db] 22 | :db/ident :order/orderid 23 | :db/valueType :db.type/long 24 | :db/cardinality :db.cardinality/one 25 | :db/unique :db.unique/value 26 | :db/doc "legacy PK" 27 | :db.install/_attribute :db.part/db} 28 | {:db/id #db/id[:db.part/db] 29 | :db/ident :order/orderdate 30 | :db/valueType :db.type/instant 31 | :db/cardinality :db.cardinality/one 32 | :db/doc "" 33 | :db.install/_attribute :db.part/db} 34 | {:db/id #db/id[:db.part/db] 35 | :db/ident :order/customerid 36 | :db/valueType :db.type/long 37 | :db/cardinality :db.cardinality/one 38 | :db/doc "legacy FK to customers.customerid" 39 | :db.install/_attribute :db.part/db} 40 | {:db/id #db/id[:db.part/db] 41 | :db/ident :order/customer 42 | :db/valueType :db.type/ref 43 | :db/cardinality :db.cardinality/one 44 | :db/doc "" 45 | :db.install/_attribute :db.part/db} 46 | {:db/id #db/id[:db.part/db] 47 | :db/ident :order/netamount 48 | :db/valueType :db.type/bigdec 49 | :db/cardinality :db.cardinality/one 50 | :db/doc "numeric(12,2)" 51 | :db.install/_attribute :db.part/db} 52 | {:db/id #db/id[:db.part/db] 53 | :db/ident :order/tax 54 | :db/valueType :db.type/bigdec 55 | :db/cardinality :db.cardinality/one 56 | :db/doc "numeric(12,2)" 57 | :db.install/_attribute :db.part/db} 58 | {:db/id #db/id[:db.part/db] 59 | :db/ident :order/totalamount 60 | :db/valueType :db.type/bigdec 61 | :db/cardinality :db.cardinality/one 62 | :db/doc "numeric(12,2)" 63 | :db.install/_attribute :db.part/db} 64 | {:db/id #db/id[:db.part/db] 65 | :db/ident :order/orderlines 66 | :db/valueType :db.type/ref 67 | :db/cardinality :db.cardinality/many 68 | :db/isComponent true 69 | :db/doc "" 70 | :db.install/_attribute :db.part/db} 71 | 72 | ;; dellstore2=# \d+ customers 73 | ;; Table "public.customers" 74 | ;; Column | Type | Modifiers | Storage | Stats target | Description 75 | ;; ----------------------+-----------------------+----------------------------------------------------------------+----------+--------------+------------- 76 | ;; customerid | integer | not null default nextval('customers_customerid_seq'::regclass) | plain | | 77 | ;; firstname | character varying(50) | not null | extended | | 78 | ;; lastname | character varying(50) | not null | extended | | 79 | ;; address1 | character varying(50) | not null | extended | | 80 | ;; address2 | character varying(50) | | extended | | 81 | ;; city | character varying(50) | not null | extended | | 82 | ;; state | character varying(50) | | extended | | 83 | ;; zip | integer | | plain | | 84 | ;; country | character varying(50) | not null | extended | | 85 | ;; region | smallint | not null | plain | | 86 | ;; email | character varying(50) | | extended | | 87 | ;; phone | character varying(50) | | extended | | 88 | ;; creditcardtype | integer | not null | plain | | 89 | ;; creditcard | character varying(50) | not null | extended | | 90 | ;; creditcardexpiration | character varying(50) | not null | extended | | 91 | ;; username | character varying(50) | not null | extended | | 92 | ;; password | character varying(50) | not null | extended | | 93 | ;; age | smallint | | plain | | 94 | ;; income | integer | | plain | | 95 | ;; gender | character varying(1) | | extended | | 96 | ;; Indexes: 97 | ;; "customers_pkey" PRIMARY KEY, btree (customerid) 98 | ;; "ix_cust_username" UNIQUE, btree (username) 99 | ;; Referenced by: 100 | ;; TABLE "cust_hist" CONSTRAINT "fk_cust_hist_customerid" FOREIGN KEY (customerid) REFERENCES customers(customerid) ON DELETE CASCADE 101 | ;; TABLE "orders" CONSTRAINT "fk_customerid" FOREIGN KEY (customerid) REFERENCES customers(customerid) ON DELETE SET NULL 102 | 103 | {:db/id #db/id[:db.part/db] 104 | :db/ident :customer/customerid 105 | :db/valueType :db.type/long 106 | :db/cardinality :db.cardinality/one 107 | :db/unique :db.unique/value 108 | :db/doc "legacy PK" 109 | :db.install/_attribute :db.part/db} 110 | {:db/id #db/id[:db.part/db] 111 | :db/ident :customer/firstname 112 | :db/valueType :db.type/string 113 | :db/cardinality :db.cardinality/one 114 | :db/doc "" 115 | :db.install/_attribute :db.part/db} 116 | {:db/id #db/id[:db.part/db] 117 | :db/ident :customer/lastname 118 | :db/valueType :db.type/string 119 | :db/cardinality :db.cardinality/one 120 | :db/doc "" 121 | :db.install/_attribute :db.part/db} 122 | {:db/id #db/id[:db.part/db] 123 | :db/ident :customer/address-1 124 | :db/valueType :db.type/string 125 | :db/cardinality :db.cardinality/one 126 | :db/doc "legacy column customers.address1" 127 | :db.install/_attribute :db.part/db} 128 | {:db/id #db/id[:db.part/db] 129 | :db/ident :customer/address-2 130 | :db/valueType :db.type/string 131 | :db/cardinality :db.cardinality/one 132 | :db/doc "legacy column customers.address2" 133 | :db.install/_attribute :db.part/db} 134 | {:db/id #db/id[:db.part/db] 135 | :db/ident :customer/city 136 | :db/valueType :db.type/string 137 | :db/cardinality :db.cardinality/one 138 | :db/doc "" 139 | :db.install/_attribute :db.part/db} 140 | {:db/id #db/id[:db.part/db] 141 | :db/ident :customer/state 142 | :db/valueType :db.type/string 143 | :db/cardinality :db.cardinality/one 144 | :db/doc "" 145 | :db.install/_attribute :db.part/db} 146 | {:db/id #db/id[:db.part/db] 147 | :db/ident :customer/zip 148 | :db/valueType :db.type/string ;; Note: legacy had this as int (!?!!!??!) 149 | :db/cardinality :db.cardinality/one 150 | :db/doc "" 151 | :db.install/_attribute :db.part/db} 152 | {:db/id #db/id[:db.part/db] 153 | :db/ident :customer/country 154 | :db/valueType :db.type/string 155 | :db/cardinality :db.cardinality/one 156 | :db/doc "" 157 | :db.install/_attribute :db.part/db} 158 | {:db/id #db/id[:db.part/db] 159 | :db/ident :customer/region 160 | :db/valueType :db.type/long 161 | :db/cardinality :db.cardinality/one 162 | :db/doc "" 163 | :db.install/_attribute :db.part/db} 164 | {:db/id #db/id[:db.part/db] 165 | :db/ident :customer/email 166 | :db/valueType :db.type/string 167 | :db/cardinality :db.cardinality/one 168 | :db/doc "" 169 | :db.install/_attribute :db.part/db} 170 | {:db/id #db/id[:db.part/db] 171 | :db/ident :customer/phone 172 | :db/valueType :db.type/string 173 | :db/cardinality :db.cardinality/one 174 | :db/doc "" 175 | :db.install/_attribute :db.part/db} 176 | {:db/id #db/id[:db.part/db] 177 | :db/ident :customer/creditcardtype ;; ???? 178 | :db/valueType :db.type/long 179 | :db/cardinality :db.cardinality/one 180 | :db/doc "" 181 | :db.install/_attribute :db.part/db} 182 | {:db/id #db/id[:db.part/db] 183 | :db/ident :customer/creditcard 184 | :db/valueType :db.type/string 185 | :db/cardinality :db.cardinality/one 186 | :db/doc "" 187 | :db.install/_attribute :db.part/db} 188 | {:db/id #db/id[:db.part/db] 189 | :db/ident :customer/creditcardexpiration 190 | :db/valueType :db.type/string 191 | :db/cardinality :db.cardinality/one 192 | :db/doc "" 193 | :db.install/_attribute :db.part/db} 194 | {:db/id #db/id[:db.part/db] 195 | :db/ident :customer/username 196 | :db/valueType :db.type/string 197 | :db/cardinality :db.cardinality/one 198 | :db/unique :db.unique/value 199 | :db/doc "" 200 | :db.install/_attribute :db.part/db} 201 | {:db/id #db/id[:db.part/db] 202 | :db/ident :customer/password 203 | :db/valueType :db.type/string 204 | :db/cardinality :db.cardinality/one 205 | :db/doc "" 206 | :db.install/_attribute :db.part/db} 207 | {:db/id #db/id[:db.part/db] 208 | :db/ident :customer/age 209 | :db/valueType :db.type/long 210 | :db/cardinality :db.cardinality/one 211 | :db/doc "" 212 | :db.install/_attribute :db.part/db} 213 | {:db/id #db/id[:db.part/db] 214 | :db/ident :customer/income 215 | :db/valueType :db.type/bigdec 216 | :db/cardinality :db.cardinality/one 217 | :db/doc "" 218 | :db.install/_attribute :db.part/db} 219 | {:db/id #db/id[:db.part/db] 220 | :db/ident :customer/gender 221 | :db/valueType :db.type/ref 222 | :db/cardinality :db.cardinality/one 223 | :db/doc "" 224 | :db.install/_attribute :db.part/db} 225 | 226 | [:db/add #db/id[:db.part/user] :db/ident :customer.gender/female] 227 | [:db/add #db/id[:db.part/user] :db/ident :customer.gender/male] 228 | 229 | ;; dellstore2=# \d+ products 230 | ;; Table "public.products" 231 | ;; Column | Type | Modifiers | Storage | Stats target | Description 232 | ;; ----------------+-----------------------+------------------------------------------------------------+----------+--------------+------------- 233 | ;; prod_id | integer | not null default nextval('products_prod_id_seq'::regclass) | plain | | 234 | ;; category | integer | not null | plain | | 235 | ;; title | character varying(50) | not null | extended | | 236 | ;; actor | character varying(50) | not null | extended | | 237 | ;; price | numeric(12,2) | not null | main | | 238 | ;; special | smallint | | plain | | 239 | ;; common_prod_id | integer | not null | plain | | 240 | ;; Indexes: 241 | ;; "products_pkey" PRIMARY KEY, btree (prod_id) 242 | ;; "ix_prod_category" btree (category) 243 | ;; "ix_prod_special" btree (special) 244 | 245 | {:db/id #db/id[:db.part/db] 246 | :db/ident :product/prod-id 247 | :db/valueType :db.type/long 248 | :db/cardinality :db.cardinality/one 249 | :db/unique :db.unique/value ;; TODO: :db.unique/identity or :db.unique/value ??? 250 | :db/doc "legacy PK products.prod_id - retained for interop" 251 | :db.install/_attribute :db.part/db} 252 | {:db/id #db/id[:db.part/db] 253 | :db/ident :product/category 254 | :db/valueType :db.type/ref 255 | :db/cardinality :db.cardinality/one 256 | :db/index true 257 | :db/doc "Product category enum" 258 | :db.install/_attribute :db.part/db} 259 | {:db/id #db/id[:db.part/db] 260 | :db/ident :product/title 261 | :db/valueType :db.type/string 262 | :db/cardinality :db.cardinality/one 263 | :db/doc "" 264 | :db.install/_attribute :db.part/db} 265 | {:db/id #db/id[db.part/db] 266 | :db/ident :product/actor 267 | :db/valueType :db.type/string 268 | :db/cardinality :db.cardinality/one 269 | :db/doc "" 270 | :db.install/_attribute :db.part/db} 271 | {:db/id #db/id[:db.part/db] 272 | :db/ident :product/price 273 | :db/valueType :db.type/bigdec ;; use BigDecimal for money 274 | :db/cardinality :db.cardinality/one 275 | :db/doc "legacy has this as numeric(12,2)" 276 | :db.install/_attribute :db.part/db} 277 | {:db/id #db/id[:db.part/db] 278 | :db/ident :product/special 279 | :db/valueType :db.type/boolean 280 | :db/cardinality :db.cardinality/one 281 | :db/index true 282 | :db/doc "legacy has this as smallint [0, 1]" 283 | :db.install/_attribute :db.part/db} 284 | {:db/id #db/id[db.part/db] 285 | :db/ident :product/common-prod-id 286 | :db/valueType :db.type/long 287 | :db/cardinality :db.cardinality/one ;; ??? or many 288 | :db/doc "not sure here; might be FK to prod_id for hierarchy; KISS" 289 | :db.install/_attribute :db.part/db} 290 | {:db/id #db/id[db.part/db] 291 | :db/ident :product/tag 292 | :db/valueType :db.type/keyword 293 | :db/cardinality :db.cardinality/one 294 | :db/doc "sample non-ident keyword field" 295 | :db.install/_attribute :db.part/db} 296 | {:db/id #db/id[db.part/db] 297 | :db/ident :product/uuid 298 | :db/valueType :db.type/uuid 299 | :db/cardinality :db.cardinality/one 300 | :db/doc "sample uuid field" 301 | :db.install/_attribute :db.part/db} 302 | {:db/id #db/id[db.part/db] 303 | :db/ident :product/url 304 | :db/valueType :db.type/uri 305 | :db/cardinality :db.cardinality/one 306 | :db/doc "sample uri field" 307 | :db.install/_attribute :db.part/db} 308 | {:db/id #db/id[db.part/db] 309 | :db/ident :product/blob 310 | :db/valueType :db.type/bytes 311 | :db/cardinality :db.cardinality/one 312 | :db/doc "sample bytes field" 313 | :db.install/_attribute :db.part/db} 314 | {:db/id #db/id[db.part/db] 315 | :db/ident :product/rating 316 | :db/valueType :db.type/float 317 | :db/cardinality :db.cardinality/one 318 | :db/doc "sample float field" 319 | :db.install/_attribute :db.part/db} 320 | {:db/id #db/id[db.part/db] 321 | :db/ident :product/man-hours 322 | :db/valueType :db.type/bigint 323 | :db/cardinality :db.cardinality/one 324 | :db/doc "sample bigint field" 325 | :db.install/_attribute :db.part/db} 326 | 327 | ;; dellstore2=# \d categories 328 | ;; Table "public.categories" 329 | ;; Column | Type | Modifiers 330 | ;; --------------+-----------------------+--------------------------------------------------------------- 331 | ;; category | integer | not null default nextval('categories_category_seq'::regclass) 332 | ;; categoryname | character varying(50) | not null 333 | ;; Indexes: 334 | ;; "categories_pkey" PRIMARY KEY, btree (category) 335 | 336 | ; Not sure this is the best approach. 337 | ; Might want to go with a more direct "table" representation. 338 | [:db/add #db/id[:db.part/user] :db/ident :product.category/action] 339 | [:db/add #db/id[:db.part/user] :db/ident :product.category/animation] 340 | [:db/add #db/id[:db.part/user] :db/ident :product.category/children] 341 | [:db/add #db/id[:db.part/user] :db/ident :product.category/classics] 342 | [:db/add #db/id[:db.part/user] :db/ident :product.category/comedy] 343 | [:db/add #db/id[:db.part/user] :db/ident :product.category/documentary] 344 | [:db/add #db/id[:db.part/user] :db/ident :product.category/drama] 345 | [:db/add #db/id[:db.part/user] :db/ident :product.category/family] 346 | [:db/add #db/id[:db.part/user] :db/ident :product.category/foreign] 347 | [:db/add #db/id[:db.part/user] :db/ident :product.category/games] 348 | [:db/add #db/id[:db.part/user] :db/ident :product.category/horror] 349 | [:db/add #db/id[:db.part/user] :db/ident :product.category/music] 350 | [:db/add #db/id[:db.part/user] :db/ident :product.category/new] 351 | [:db/add #db/id[:db.part/user] :db/ident :product.category/sci-fi] 352 | [:db/add #db/id[:db.part/user] :db/ident :product.category/sports] 353 | [:db/add #db/id[:db.part/user] :db/ident :product.category/travel] 354 | 355 | 356 | 357 | ;; dellstore2=# \d+ orderlines 358 | ;; Table "public.orderlines" 359 | ;; Column | Type | Modifiers | Storage | Stats target | Description 360 | ;; -------------+----------+-----------+---------+--------------+------------- 361 | ;; orderlineid | integer | not null | plain | | 362 | ;; orderid | integer | not null | plain | | 363 | ;; prod_id | integer | not null | plain | | 364 | ;; quantity | smallint | not null | plain | | 365 | ;; orderdate | date | not null | plain | | 366 | ;; Indexes: 367 | ;; "ix_orderlines_orderid" UNIQUE, btree (orderid, orderlineid) 368 | ;; Foreign-key constraints: 369 | ;; "fk_orderid" FOREIGN KEY (orderid) REFERENCES orders(orderid) ON DELETE CASCADE 370 | 371 | {:db/id #db/id[:db.part/db] 372 | :db/ident :orderline/orderlineid 373 | :db/valueType :db.type/long 374 | :db/cardinality :db.cardinality/one 375 | :db/doc "legacy composite PK with orderid" 376 | :db.install/_attribute :db.part/db} 377 | {:db/id #db/id[:db.part/db] 378 | :db/ident :orderline/orderid 379 | :db/valueType :db.type/long 380 | :db/cardinality :db.cardinality/one 381 | :db/doc "legacy FK to orders.orderid" 382 | :db.install/_attribute :db.part/db} 383 | {:db/id #db/id[:db.part/db] 384 | :db/ident :orderline/prod-id 385 | :db/valueType :db.type/long 386 | :db/cardinality :db.cardinality/one 387 | :db/doc "legacy FK to products.prod_id" 388 | :db.install/_attribute :db.part/db} 389 | {:db/id #db/id[:db.part/db] 390 | :db/ident :orderline/product 391 | :db/valueType :db.type/ref 392 | :db/cardinality :db.cardinality/one 393 | :db/doc "" 394 | :db.install/_attribute :db.part/db} 395 | {:db/id #db/id[:db.part/db] 396 | :db/ident :orderline/quantity 397 | :db/valueType :db.type/long 398 | :db/cardinality :db.cardinality/one 399 | :db/doc "" 400 | :db.install/_attribute :db.part/db} 401 | {:db/id #db/id[:db.part/db] 402 | :db/ident :orderline/orderdate 403 | :db/valueType :db.type/instant 404 | :db/cardinality :db.cardinality/one 405 | :db/doc "" 406 | :db.install/_attribute :db.part/db} 407 | 408 | 409 | 410 | 411 | ;; dellstore2=# \d+ inventory 412 | ;; Table "public.inventory" 413 | ;; Column | Type | Modifiers | Storage | Stats target | Description 414 | ;; ---------------+---------+-----------+---------+--------------+------------- 415 | ;; prod_id | integer | not null | plain | | 416 | ;; quan_in_stock | integer | not null | plain | | 417 | ;; sales | integer | not null | plain | | 418 | ;; Indexes: 419 | ;; "inventory_pkey" PRIMARY KEY, btree (prod_id) 420 | 421 | {:db/id #db/id[:db.part/db] 422 | :db/ident :inventory/product 423 | :db/valueType :db.type/ref 424 | :db/cardinality :db.cardinality/one 425 | :db/unique :db.unique/value 426 | :db/doc "" 427 | :db.install/_attribute :db.part/db} 428 | {:db/id #db/id[:db.part/db] 429 | :db/ident :inventory/prod-id 430 | :db/valueType :db.type/long 431 | :db/cardinality :db.cardinality/one 432 | :db/unique :db.unique/value 433 | :db/doc "legacy FK to products.prod_id" 434 | :db.install/_attribute :db.part/db} 435 | {:db/id #db/id[:db.part/db] 436 | :db/ident :inventory/quan-in-stock 437 | :db/valueType :db.type/long 438 | :db/cardinality :db.cardinality/one 439 | :db/doc "" 440 | :db.install/_attribute :db.part/db} 441 | {:db/id #db/id[:db.part/db] 442 | :db/ident :inventory/sales 443 | :db/valueType :db.type/long 444 | :db/cardinality :db.cardinality/one 445 | :db/doc "" 446 | :db.install/_attribute :db.part/db} 447 | 448 | ;; dellstore2=# \d+ cust_hist 449 | ;; Table "public.cust_hist" 450 | ;; Column | Type | Modifiers | Storage | Stats target | Description 451 | ;; ------------+---------+-----------+---------+--------------+------------- 452 | ;; customerid | integer | not null | plain | | 453 | ;; orderid | integer | not null | plain | | 454 | ;; prod_id | integer | not null | plain | | 455 | ;; Indexes: 456 | ;; "ix_cust_hist_customerid" btree (customerid) 457 | ;; Foreign-key constraints: 458 | ;; "fk_cust_hist_customerid" FOREIGN KEY (customerid) REFERENCES customers(customerid) ON DELETE CASCADE 459 | 460 | {:db/id #db/id[:db.part/db] 461 | :db/ident :customer-history/customerid 462 | :db/valueType :db.type/long 463 | :db/cardinality :db.cardinality/one 464 | :db/doc "legacy FK to customers.customerid" 465 | :db.install/_attribute :db.part/db} 466 | {:db/id #db/id[:db.part/db] 467 | :db/ident :customer-history/order 468 | :db/valueType :db.type/ref 469 | :db/cardinality :db.cardinality/one 470 | :db/doc "" 471 | :db.install/_attribute :db.part/db} 472 | {:db/id #db/id[:db.part/db] 473 | :db/ident :customer-history/orderid 474 | :db/valueType :db.type/long 475 | :db/cardinality :db.cardinality/one 476 | :db/doc "legacy FK to orders.orderid" 477 | :db.install/_attribute :db.part/db} 478 | {:db/id #db/id[:db.part/db] 479 | :db/ident :customer-history/product 480 | :db/valueType :db.type/ref 481 | :db/cardinality :db.cardinality/one 482 | :db/doc "" 483 | :db.install/_attribute :db.part/db} 484 | {:db/id #db/id[:db.part/db] 485 | :db/ident :customer-history/prod-id 486 | :db/valueType :db.type/long 487 | :db/cardinality :db.cardinality/one 488 | :db/doc "legacy FK to products.prod_id" 489 | :db.install/_attribute :db.part/db} 490 | {:db/id #db/id[:db.part/db] 491 | :db/ident :customer-history/customer 492 | :db/valueType :db.type/ref 493 | :db/cardinality :db.cardinality/one 494 | :db/doc "" 495 | :db.install/_attribute :db.part/db} 496 | 497 | ;; dellstore2=# \d+ reorder 498 | ;; Table "public.reorder" 499 | ;; Column | Type | Modifiers | Storage | Stats target | Description 500 | ;; ----------------+---------+-----------+---------+--------------+------------- 501 | ;; prod_id | integer | not null | plain | | 502 | ;; date_low | date | not null | plain | | 503 | ;; quan_low | integer | not null | plain | | 504 | ;; date_reordered | date | | plain | | 505 | ;; quan_reordered | integer | | plain | | 506 | ;; date_expected | date | | plain | | 507 | {:db/id #db/id[:db.part/db] 508 | :db/ident :reorder/prod-id 509 | :db/valueType :db.type/long 510 | :db/cardinality :db.cardinality/one 511 | :db/doc "legacy FK" 512 | :db.install/_attribute :db.part/db} 513 | {:db/id #db/id[:db.part/db] 514 | :db/ident :reorder/product 515 | :db/valueType :db.type/ref 516 | :db/cardinality :db.cardinality/one 517 | :db/doc "" 518 | :db.install/_attribute :db.part/db} 519 | {:db/id #db/id[:db.part/db] 520 | :db/ident :reorder/date-low 521 | :db/valueType :db.type/instant 522 | :db/cardinality :db.cardinality/one 523 | :db/doc "" 524 | :db.install/_attribute :db.part/db} 525 | {:db/id #db/id[:db.part/db] 526 | :db/ident :reorder/quantity-low 527 | :db/valueType :db.type/long 528 | :db/cardinality :db.cardinality/one 529 | :db/doc "-> reorder.quan_low" 530 | :db.install/_attribute :db.part/db} 531 | {:db/id #db/id[:db.part/db] 532 | :db/ident :reorder/date-reordered 533 | :db/valueType :db.type/instant 534 | :db/cardinality :db.cardinality/one 535 | :db/doc "" 536 | :db.install/_attribute :db.part/db} 537 | {:db/id #db/id[:db.part/db] 538 | :db/ident :reorder/quantity-reordered 539 | :db/valueType :db.type/long 540 | :db/cardinality :db.cardinality/one 541 | :db/doc "-> reorder.quan_reordered" 542 | :db.install/_attribute :db.part/db} 543 | {:db/id #db/id[:db.part/db] 544 | :db/ident :reorder/date-expected 545 | :db/valueType :db.type/instant 546 | :db/cardinality :db.cardinality/one 547 | :db/doc "" 548 | :db.install/_attribute :db.part/db} 549 | ] 550 | -------------------------------------------------------------------------------- /resources/seattle-schema.edn: -------------------------------------------------------------------------------- 1 | [ 2 | ;; community 3 | 4 | {:db/id #db/id[:db.part/db] 5 | :db/ident :community/name 6 | :db/valueType :db.type/string 7 | :db/cardinality :db.cardinality/one 8 | :db/fulltext true 9 | :db/doc "A community's name" 10 | :db.install/_attribute :db.part/db} 11 | 12 | {:db/id #db/id[:db.part/db] 13 | :db/ident :community/url 14 | :db/valueType :db.type/string 15 | :db/cardinality :db.cardinality/one 16 | :db/doc "A community's url" 17 | :db.install/_attribute :db.part/db} 18 | 19 | {:db/id #db/id[:db.part/db] 20 | :db/ident :community/neighborhood 21 | :db/valueType :db.type/ref 22 | :db/cardinality :db.cardinality/one 23 | :db/doc "A community's neighborhood" 24 | :db.install/_attribute :db.part/db} 25 | 26 | {:db/id #db/id[:db.part/db] 27 | :db/ident :community/category 28 | :db/valueType :db.type/string 29 | :db/cardinality :db.cardinality/many 30 | :db/fulltext true 31 | :db/doc "All community categories" 32 | :db.install/_attribute :db.part/db} 33 | 34 | {:db/id #db/id[:db.part/db] 35 | :db/ident :community/orgtype 36 | :db/valueType :db.type/ref 37 | :db/cardinality :db.cardinality/one 38 | :db/doc "A community orgtype enum value" 39 | :db.install/_attribute :db.part/db} 40 | 41 | {:db/id #db/id[:db.part/db] 42 | :db/ident :community/type 43 | :db/valueType :db.type/ref 44 | :db/cardinality :db.cardinality/many 45 | :db/doc "Community type enum values" 46 | :db.install/_attribute :db.part/db} 47 | 48 | ;; community/org-type enum values 49 | [:db/add #db/id[:db.part/user] :db/ident :community.orgtype/community] 50 | [:db/add #db/id[:db.part/user] :db/ident :community.orgtype/commercial] 51 | [:db/add #db/id[:db.part/user] :db/ident :community.orgtype/nonprofit] 52 | [:db/add #db/id[:db.part/user] :db/ident :community.orgtype/personal] 53 | 54 | ;; community/type enum values 55 | [:db/add #db/id[:db.part/user] :db/ident :community.type/email-list] 56 | [:db/add #db/id[:db.part/user] :db/ident :community.type/twitter] 57 | [:db/add #db/id[:db.part/user] :db/ident :community.type/facebook-page] 58 | [:db/add #db/id[:db.part/user] :db/ident :community.type/blog] 59 | [:db/add #db/id[:db.part/user] :db/ident :community.type/website] 60 | [:db/add #db/id[:db.part/user] :db/ident :community.type/wiki] 61 | [:db/add #db/id[:db.part/user] :db/ident :community.type/myspace] 62 | [:db/add #db/id[:db.part/user] :db/ident :community.type/ning] 63 | 64 | ;; neighborhood 65 | {:db/id #db/id[:db.part/db] 66 | :db/ident :neighborhood/name 67 | :db/valueType :db.type/string 68 | :db/cardinality :db.cardinality/one 69 | :db/unique :db.unique/identity 70 | :db/doc "A unique neighborhood name (upsertable)" 71 | :db.install/_attribute :db.part/db} 72 | 73 | {:db/id #db/id[:db.part/db] 74 | :db/ident :neighborhood/district 75 | :db/valueType :db.type/ref 76 | :db/cardinality :db.cardinality/one 77 | :db/doc "A neighborhood's district" 78 | :db.install/_attribute :db.part/db} 79 | 80 | ;; district 81 | {:db/id #db/id[:db.part/db] 82 | :db/ident :district/name 83 | :db/valueType :db.type/string 84 | :db/cardinality :db.cardinality/one 85 | :db/unique :db.unique/identity 86 | :db/doc "A unique district name (upsertable)" 87 | :db.install/_attribute :db.part/db} 88 | 89 | {:db/id #db/id[:db.part/db] 90 | :db/ident :district/region 91 | :db/valueType :db.type/ref 92 | :db/cardinality :db.cardinality/one 93 | :db/doc "A district region enum value" 94 | :db.install/_attribute :db.part/db} 95 | 96 | ;; district/region enum values 97 | [:db/add #db/id[:db.part/user] :db/ident :region/n] 98 | [:db/add #db/id[:db.part/user] :db/ident :region/ne] 99 | [:db/add #db/id[:db.part/user] :db/ident :region/e] 100 | [:db/add #db/id[:db.part/user] :db/ident :region/se] 101 | [:db/add #db/id[:db.part/user] :db/ident :region/s] 102 | [:db/add #db/id[:db.part/user] :db/ident :region/sw] 103 | [:db/add #db/id[:db.part/user] :db/ident :region/w] 104 | [:db/add #db/id[:db.part/user] :db/ident :region/nw] 105 | ] 106 | -------------------------------------------------------------------------------- /resources/sql-eensy.bnf: -------------------------------------------------------------------------------- 1 | sql_data_statement ::= delete_statement 2 | | select_statement 3 | | insert_statement 4 | | update_statement 5 | | retract_statement 6 | 7 | delete_statement ::= 8 | <"DELETE"> <"FROM"> table_name 9 | [ where_clause ] 10 | | <"DELETE"> where_clause 11 | 12 | insert_statement ::= 13 | <"INSERT"> <"INTO"> table_name 14 | <"("> insert_cols <")"> 15 | <"VALUES"> 16 | <"("> insert_vals <")"> 17 | | <"INSERT"> [<"INTO">] 18 | set_clausen 19 | 20 | select_statement ::= <"SELECT"> select_list from_clause [ where_clause ] 21 | | <"SELECT"> [ select_list ] where_clause 22 | 23 | update_statement ::= 24 | <"UPDATE"> table_name <"SET"> set_clausen [ where_clause ] 25 | | <"UPDATE"> [ <"SET"> ] set_clausen where_clause 26 | 27 | (* reduce ease-of-use intentionally via db_id_clause and not where_clause 28 | in order to make the user more mindful of these changes *) 29 | retract_statement ::= 30 | <"RETRACT"> retract_attrs <"WHERE"> db_id_clause 31 | 32 | table_name ::= #"[-_A-Za-z][-\w]*" 33 | 34 | search_condition ::= boolean_term 35 | | search_condition <"OR"> boolean_term 36 | 37 | boolean_term ::= boolean_factor 38 | | boolean_term <"AND"> boolean_factor 39 | 40 | boolean_negative ::= <"NOT"> boolean_test 41 | 42 | boolean_factor ::= boolean_negative | boolean_test 43 | 44 | boolean_test ::= boolean_primary 45 | 46 | ::= predicate | <"("> search_condition <")"> 47 | 48 | ::= binary_comparison 49 | | between_clause 50 | | in_clause 51 | 52 | (* disallow mixing of db_id-based where with general where *) 53 | where_clause ::= <"WHERE"> ( db_id_clause | search_condition ) 54 | 55 | db_id_clause ::= < ("db.id" | <"#attr"> ":db/id") > 56 | ( 57 | <"="> long_literal 58 | | 59 | <"IN"> <"("> 60 | long_literal [ { [<",">] long_literal }+ ] <")"> 61 | | (* be flexible with :db/id selections *) 62 | long_literal [ { [<",">] long_literal }+ ] 63 | ) 64 | 65 | insert_cols ::= #"[-_A-Za-z][-\w]*" [ { <","> #"[-_A-Za-z][-\w]*" }+ ] 66 | 67 | ::= boolean_literal 68 | | bytes_literal 69 | | chrono_literal 70 | | keyword_literal 71 | | numeric_literal 72 | | string_literal 73 | | uri_literal 74 | | uuid_literal 75 | 76 | insert_vals ::= insertable_val [ { <","> insertable_val }+ ] 77 | 78 | from_clause ::= <"FROM"> table_ref [ { <","> table_ref }+ ] 79 | 80 | ::= column_name 81 | | qualified_asterisk 82 | | boolean_literal 83 | | bytes_literal 84 | | chrono_literal 85 | | numeric_literal 86 | | string_literal 87 | | uri_literal 88 | | uuid_literal 89 | 90 | select_list ::= selectable_val [ { <","> selectable_val }+ ] 91 | 92 | set_clausen ::= assignment_pair [ { <","> assignment_pair }+ ] 93 | 94 | retract_attrs ::= non_db_id_column_name [ { <","> non_db_id_column_name }+ ] 95 | 96 | between_clause ::= non_db_id_column_name <"BETWEEN"> 97 | { 98 | ( chrono_literal <"AND"> chrono_literal ) 99 | | 100 | ( numeric_literal <"AND"> numeric_literal ) 101 | | 102 | ( uuid_literal <"AND"> uuid_literal ) 103 | } 104 | 105 | binary_comparison ::= non_db_id_column_name 106 | { 107 | ( #"(<=|<|>=|>)" ( non_db_id_column_name 108 | | comparable_literal ) ) 109 | | 110 | ( #"(=|!=|<>)" ( non_db_id_column_name 111 | | equalable_literal ) ) 112 | } 113 | 114 | in_clause ::= non_db_id_column_name <"IN"> <"("> 115 | equalable_literal [ { <","> equalable_literal }+ ] 116 | <")"> 117 | 118 | string_literal ::= #"'(?:[^'\\]|\\.)*?'" 119 | 120 | long_literal ::= #"(?:-|\+)?(?:0|[1-9]\d*)" 121 | 122 | bigint_literal ::= #"(?:-|\+)?(?:0|[1-9]\d*)N" 123 | 124 | double_literal ::= #"(?:(?i)[-+]?(?:\d(?:\.\d*)?[Ee][-+]?\d+|\d+\.\d*))" 125 | 126 | float_literal ::= #"(?:(?i)[-+]?(?:\d(?:\.\d*)?[Ee][-+]?\d+|\d+\.\d*))[Ff]" 127 | | <"#float"> #"(?:(?i)[-+]?(?:\d(?:\.\d*)?[Ee][-+]?\d+|\d+\.\d*))" 128 | 129 | bigdec_literal ::= #"(?:(?i)[-+]?(?:\d(?:\.\d*)?[Ee][-+]?\d+|\d+(?:\.\d*)?))M" 130 | 131 | boolean_literal ::= true | false 132 | 133 | true ::= <"TRUE"> 134 | 135 | false ::= <"FALSE"> 136 | 137 | inst_literal ::= <"#inst"> 138 | <"\""> 139 | #"\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?([-\+]\d{2}:\d{2})?)?" 140 | <"\""> 141 | 142 | date_literal ::= <"date"> <"'"> #"\d{4}-\d{2}-\d{2}" <"'"> 143 | 144 | datetime_literal ::= <"datetime"> 145 | <"'"> 146 | #"\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}" 147 | <"'"> 148 | 149 | epochal_literal ::= <"time"> <"'"> #"\d+" <"'"> 150 | 151 | keyword_literal ::= <":"> #"[-\.\*\+\$\?\|_/<>=%!#\w]+" 152 | 153 | uuid_literal ::= <"#uuid"> 154 | <"\""> 155 | #"[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}" 156 | <"\""> 157 | 158 | uri_literal ::= <"#uri"> <"\""> #"[^\"]+" <"\""> 159 | 160 | bytes_literal ::= <#"#(bytes|base64)"> <"\""> #"[A-Za-z0-9+/=]+" <"\""> 161 | 162 | table_ref ::= table_name [ table_alias ] 163 | 164 | ::= 165 | ! #"db.id|#attr\s+:db/id" 166 | column_name 167 | 168 | column_name ::= 169 | #'("[^\"]+"|[-_A-Za-z][-\w]*)' 170 | <"."> #'("[^\"]+"|[-_A-Za-z][-\w]*)' 171 | | <"#attr"> <":"> 172 | #"([-\.\*\+\$\?\|_<>=%!#\w]+)" 173 | <"/"> 174 | #"([-\.\*\+\$\?\|_<>=%!#\w]+)" 175 | 176 | qualified_asterisk ::= #'("[^\"]+"|[-_A-Za-z][-\w]*)' <"."> <"*"> 177 | 178 | assignment_pair ::= non_db_id_column_name <"="> 179 | ( string_literal 180 | | boolean_literal 181 | | numeric_literal 182 | | keyword_literal 183 | | chrono_literal 184 | | uuid_literal 185 | | uri_literal 186 | | bytes_literal ) 187 | 188 | table_alias ::= [ <"AS"> ] #'"[^\"]+"|[-_A-Za-z][-\w]*' 189 | 190 | ::= long_literal | bigint_literal 191 | 192 | ::= float_literal | double_literal | bigdec_literal 193 | 194 | ::= integral_literal | floating_literal 195 | 196 | ::= date_literal 197 | | datetime_literal 198 | | epochal_literal 199 | | inst_literal 200 | 201 | (* literals supporting `=`, `<>`, `!=` *) 202 | ::= keyword_literal 203 | | boolean_literal 204 | | chrono_literal 205 | | numeric_literal 206 | | string_literal 207 | | uuid_literal 208 | | uri_literal 209 | 210 | (* literals supporting `<`, `<=`, `>`, `>=` *) 211 | ::= chrono_literal 212 | | numeric_literal 213 | | uuid_literal 214 | -------------------------------------------------------------------------------- /resources/starfighter-data.edn: -------------------------------------------------------------------------------- 1 | [{:db/id #db/id[:db.part/user] 2 | :starfighter/ship-id 1000 3 | :starfighter/ship-class :starfighter.ship-class/x-wing 4 | :pilot/name "Wedge Antilles" 5 | :alliance/uuid #uuid "15094ef3-0350-3edf-9246-70a34ecd8644" 6 | :kills/capital-ships 4N 7 | :kills/battle-stations 1 8 | :ship/warp-capable? true 9 | :ship/shields-capable? true 10 | :maneuverability/rating #float 7.0 11 | :starfighter/armament #{:starfighter.armament/laser-cannon 12 | :starfighter.armament/photon-torpedo}} 13 | {:db/id #db/id[:db.part/user] 14 | :starfighter/ship-id 1001 15 | :starfighter/ship-class :starfighter.ship-class/x-wing 16 | :pilot/name "Chump McDeadface" 17 | :alliance/uuid #uuid "ee08df6e-64cf-3809-8001-e50eb79c38f7" 18 | :kills/capital-ships 0N 19 | :kills/battle-stations 0 20 | :ship/warp-capable? true 21 | :ship/shields-capable? false ;; ohnoes 22 | :maneuverability/rating #float 5.0 23 | :starfighter/armament #{:starfighter.armament/laser-cannon 24 | :starfighter.armament/concussion-missle}} 25 | {:db/id #db/id[:db.part/user] 26 | :starfighter/ship-id 1002 27 | :starfighter/ship-class :starfighter.ship-class/y-wing 28 | :pilot/name "Yellow Leader" 29 | :alliance/uuid #uuid "f42dafac-e21f-307a-b62f-2006c2aa081e" 30 | :kills/capital-ships 20N 31 | :kills/battle-stations 5 32 | :ship/warp-capable? true 33 | :ship/shields-capable? true 34 | :maneuverability/rating #float 3.5 35 | :starfighter/armament #{:starfighter.armament/laser-cannon 36 | :starfighter.armament/ion-cannon 37 | :starfighter.armament/photon-torpedo 38 | :starfighter.armament/concussion-missle}} 39 | {:db/id #db/id[:db.part/user] 40 | :starfighter/ship-id 1003 41 | :starfighter/ship-class :starfighter.ship-class/a-wing 42 | :pilot/name "Gold Leader" 43 | :alliance/uuid #uuid "1ee67752-e94a-3eda-b978-def5b5373bca" 44 | :kills/capital-ships 5N 45 | :kills/battle-stations 1 46 | :ship/warp-capable? true 47 | :ship/shields-capable? true 48 | :maneuverability/rating #float 8.2 49 | :starfighter/armament #{:starfighter.armament/laser-cannon 50 | :starfighter.armament/photon-torpedo 51 | :starfighter.armament/concussion-missle}} 52 | {:db/id #db/id[:db.part/user] 53 | :starfighter/ship-id 1004 54 | :starfighter/ship-class :starfighter.ship-class/b-wing 55 | :pilot/name "Blue Leader" 56 | :alliance/uuid #uuid "663c9313-0dab-3eca-ac86-dedf403f30cc" 57 | :kills/capital-ships 7N 58 | :kills/battle-stations 2 59 | :ship/warp-capable? true 60 | :ship/shields-capable? true 61 | :maneuverability/rating #float 6.5 62 | :starfighter/armament #{:starfighter.armament/laser-cannon 63 | :starfighter.armament/ion-cannon 64 | :starfighter.armament/photon-torpedo 65 | :starfighter.armament/concussion-missle}}] 66 | -------------------------------------------------------------------------------- /resources/starfighter-schema.edn: -------------------------------------------------------------------------------- 1 | ;; Schema to demonstrate entities having heterogenous attribute namespaces. 2 | [{:db/id #db/id[:db.part/db] 3 | :db/ident :starfighter/ship-id 4 | :db/valueType :db.type/long 5 | :db/unique :db.unique/value 6 | :db/cardinality :db.cardinality/one 7 | :db/doc "" 8 | :db.install/_attribute :db.part/db} 9 | {:db/id #db/id[:db.part/db] 10 | :db/ident :kills/capital-ships 11 | :db/valueType :db.type/bigint 12 | :db/cardinality :db.cardinality/one 13 | :db/doc "" 14 | :db.install/_attribute :db.part/db} 15 | {:db/id #db/id[:db.part/db] 16 | :db/ident :ship/warp-capable? 17 | :db/valueType :db.type/boolean 18 | :db/cardinality :db.cardinality/one 19 | :db/doc "" 20 | :db.install/_attribute :db.part/db} 21 | {:db/id #db/id[:db.part/db] 22 | :db/ident :ship/shields-capable? 23 | :db/valueType :db.type/boolean 24 | :db/cardinality :db.cardinality/one 25 | :db/doc "" 26 | :db.install/_attribute :db.part/db} 27 | {:db/id #db/id[:db.part/db] 28 | :db/ident :starfighter/ship-class 29 | :db/valueType :db.type/ref 30 | :db/cardinality :db.cardinality/one 31 | :db/doc "" 32 | :db.install/_attribute :db.part/db} 33 | {:db/id #db/id[:db.part/db] 34 | :db/ident :maneuverability/rating 35 | :db/valueType :db.type/float 36 | :db/cardinality :db.cardinality/one 37 | :db/doc "" 38 | :db.install/_attribute :db.part/db} 39 | {:db/id #db/id[:db.part/db] 40 | :db/ident :kills/battle-stations 41 | :db/valueType :db.type/long 42 | :db/cardinality :db.cardinality/one 43 | :db/doc "" 44 | :db.install/_attribute :db.part/db} 45 | {:db/id #db/id[:db.part/db] 46 | :db/ident :pilot/name 47 | :db/valueType :db.type/string 48 | :db/cardinality :db.cardinality/one 49 | :db/doc "" 50 | :db.install/_attribute :db.part/db} 51 | {:db/id #db/id[:db.part/db] 52 | :db/ident :starfighter/armament 53 | :db/valueType :db.type/ref 54 | :db/cardinality :db.cardinality/many 55 | :db/doc "" 56 | :db.install/_attribute :db.part/db} 57 | {:db/id #db/id[:db.part/db] 58 | :db/ident :alliance/uuid 59 | :db/valueType :db.type/uuid 60 | :db/cardinality :db.cardinality/one 61 | :db/doc "" 62 | :db.install/_attribute :db.part/db} 63 | 64 | [:db/add #db/id[:db.part/user] 65 | :db/ident :starfighter.ship-class/x-wing] 66 | [:db/add #db/id[:db.part/user] 67 | :db/ident :starfighter.ship-class/y-wing] 68 | [:db/add #db/id[:db.part/user] 69 | :db/ident :starfighter.ship-class/a-wing] 70 | [:db/add #db/id[:db.part/user] 71 | :db/ident :starfighter.ship-class/b-wing] 72 | 73 | [:db/add #db/id[:db.part/user] 74 | :db/ident :starfighter.armament/laser-cannon] 75 | [:db/add #db/id[:db.part/user] 76 | :db/ident :starfighter.armament/ion-cannon] 77 | [:db/add #db/id[:db.part/user] 78 | :db/ident :starfighter.armament/photon-torpedo] 79 | [:db/add #db/id[:db.part/user] 80 | :db/ident :starfighter.armament/concussion-missle]] 81 | -------------------------------------------------------------------------------- /src/data_readers.clj: -------------------------------------------------------------------------------- 1 | {uri sql-datomic.types/->uri 2 | bytes sql-datomic.types/base64-str->bytes 3 | float sql-datomic.types/->float} 4 | -------------------------------------------------------------------------------- /src/sql_datomic/datomic.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.datomic 2 | (:require [datomic.api :as d] 3 | [clojure.edn :as edn] 4 | [com.stuartsierra.component :as component] 5 | [clojure.pprint :as pp] 6 | [clojure.walk :as walk] 7 | [sql-datomic.util :as util :refer [get-entities-by-eids]]) 8 | (:import [datomic.impl Exceptions$IllegalArgumentExceptionInfo])) 9 | 10 | (def default-connection-uri "datomic:mem://somewhere") 11 | 12 | (def rules 13 | '[ 14 | ;; (between 2 ?foo-id 4) 15 | ;; (between #inst "2004-01-09T00:00:00.000-00:00" 16 | ;; ?od #inst "2004-01-10T00:00:00.000-00:00" 17 | [[between ?v1 ?c ?v2] 18 | [(<= ?v1 ?c)] 19 | [(<= ?c ?v2)]] 20 | 21 | ;; (unify-ident :product.category/action ?ident2974) 22 | [[unify-ident ?ident ?var] 23 | [(datomic.api/entid $ ?ident) ?var]] 24 | 25 | ;; 26 | #_[[db-id= ?eid ?var] 27 | [(ground ?eid) ?var] 28 | [?var]] 29 | ]) 30 | 31 | (declare datomicify-clause) 32 | 33 | (defn -create-dellstore-db [] 34 | (d/create-database default-connection-uri) 35 | (let [connection (d/connect default-connection-uri) 36 | load-edn (fn [path] 37 | (->> path 38 | slurp 39 | (edn/read-string {:readers *data-readers*}))) 40 | schema-tx (load-edn "resources/dellstore-schema.edn") 41 | customers-tx (load-edn "resources/dellstore-customers-data.edn") 42 | products-tx (load-edn "resources/dellstore-products-data.edn") 43 | orders-tx (load-edn "resources/dellstore-orders-data.edn")] 44 | @(d/transact connection schema-tx) 45 | ;; Order here matters. 46 | @(d/transact connection customers-tx) 47 | @(d/transact connection products-tx) 48 | @(d/transact connection orders-tx) 49 | connection)) 50 | 51 | (defn -create-starfighter-db [] 52 | (d/create-database default-connection-uri) 53 | (let [connection (d/connect default-connection-uri) 54 | load-edn (fn [path] 55 | (->> path 56 | slurp 57 | (edn/read-string {:readers *data-readers*}))) 58 | schema-tx (load-edn "resources/starfighter-schema.edn") 59 | data-tx (load-edn "resources/starfighter-data.edn")] 60 | @(d/transact connection schema-tx) 61 | @(d/transact connection data-tx) 62 | connection)) 63 | 64 | (defn -create-seattle-db [] 65 | (d/create-database default-connection-uri) 66 | (let [connection (d/connect default-connection-uri) 67 | load-edn (fn [path] 68 | (->> path 69 | slurp 70 | (edn/read-string {:readers *data-readers*}))) 71 | schema-tx (load-edn "resources/seattle-schema.edn") 72 | data0-tx (load-edn "resources/seattle-data0.edn") 73 | data1-tx (load-edn "resources/seattle-data1.edn")] 74 | @(d/transact connection schema-tx) 75 | @(d/transact connection data0-tx) 76 | @(d/transact connection data1-tx) 77 | connection)) 78 | 79 | (defn create-default-db 80 | ([] (create-default-db :dellstore)) 81 | ([which-schema] 82 | (case which-schema 83 | :starfighter (-create-starfighter-db) 84 | :seattle (-create-seattle-db) 85 | :dellstore (-create-dellstore-db) 86 | ;; else 87 | (-create-dellstore-db)))) 88 | 89 | (defn delete-default-db [] 90 | (d/delete-database default-connection-uri)) 91 | 92 | (defn recreate-default-db 93 | ([] (recreate-default-db :dellstore)) 94 | ([which-schema] 95 | (delete-default-db) 96 | (create-default-db which-schema))) 97 | 98 | (def default-uri? (partial = default-connection-uri)) 99 | 100 | (defrecord DatomicConnection [connection-uri connection schema-name] 101 | component/Lifecycle 102 | (start [component] 103 | (if (default-uri? (:connection-uri component)) 104 | (assoc component :connection (create-default-db schema-name)) 105 | (assoc component :connection (d/connect (:connection-uri component))))) 106 | (stop [component] 107 | (when (default-uri? (:connection-uri component)) 108 | (delete-default-db)) 109 | (assoc component :connection nil))) 110 | 111 | (defn system [{:keys [connection connection-uri schema-name] 112 | :or {connection-uri default-connection-uri}}] 113 | (component/system-map 114 | :datomic (map->DatomicConnection {:connection connection 115 | :connection-uri connection-uri 116 | :schema-name schema-name}))) 117 | 118 | (defn table-column->attr-kw [{:keys [table column]}] 119 | (keyword table column)) 120 | 121 | (defonce db-id-column {:table "db", :column "id"}) 122 | 123 | (defn db-id? [c] (= c db-id-column)) 124 | 125 | (defn column? [v] 126 | (and (map? v) 127 | (= #{:table :column} 128 | (set (keys v))))) 129 | 130 | (defn extract-columns 131 | "Returns a set of `{:table t, :column c}`." 132 | [where-clauses] 133 | (let [extract-from-clause (fn [clause] 134 | (->> (tree-seq coll? seq clause) 135 | (filter column?)))] 136 | (->> where-clauses 137 | (mapcat extract-from-clause) 138 | (into #{})))) 139 | 140 | (defn gensym-datomic-entity-var [] 141 | (gensym "?e")) 142 | 143 | (defn gensym-datomic-value-var [] 144 | (gensym "?v")) 145 | 146 | (defn gensym-datomic-ident-var [] 147 | (gensym "?ident")) 148 | 149 | (defn datomic-var? [v] 150 | (and (symbol? v) (re-seq #"^\?" (name v)))) 151 | 152 | (defn scrape-datomic-vars [tree] 153 | (->> (tree-seq coll? seq tree) 154 | (filter datomic-var?) 155 | (into #{}))) 156 | 157 | (defn tag-entity-var [var name] 158 | (vary-meta var update :ir (fnil conj #{}) name)) 159 | 160 | (defn build-datomic-var-map 161 | "Returns a map, keyed by `{:table t, :column c}` with vals 162 | `{:entity ?e1234, :value ?v2341, :attr :foo/bar}`" 163 | [columns] 164 | ;; TODO: How do we deal with table aliases? 165 | ;; This will naively unify based on table name alone. 166 | (let [name->entity (->> columns 167 | (group-by :table) 168 | (map (fn [[name _]] 169 | [name (-> (gensym-datomic-entity-var) 170 | (tag-entity-var name))])) 171 | (into {}))] 172 | (->> columns 173 | (map (fn [col] [col {:entity (get name->entity (:table col)) 174 | :value (gensym-datomic-value-var) 175 | :attr (table-column->attr-kw col)}])) 176 | (into {})))) 177 | 178 | (defn binary-comparison->datomic 179 | [{:keys [col->var ident-env op operands]}] 180 | (let [[c v] operands 181 | {c-sym :value} (get col->var c :unknown-column!) 182 | v' (if-let [v-sym (get col->var v)] 183 | (:value v-sym) 184 | (get-in ident-env [v :var] v)) 185 | kw->op {:= '= 186 | :not= 'not= 187 | :< '< 188 | :> '> 189 | :<= '<= 190 | :>= '>=}] 191 | [(list (kw->op op) c-sym v')])) 192 | 193 | (defn between->datomic [{:keys [col->var operands]}] 194 | (let [[c v1 v2] operands 195 | {v-sym :value} (get col->var c :unknown-column!)] 196 | (list 'between v1 v-sym v2))) 197 | 198 | (defn lookup-ref? [r] 199 | (and (vector? r) 200 | (->> r first keyword?) 201 | (->> r second ((complement coll?))))) 202 | 203 | (defn ident-value [db v] 204 | (and v 205 | (not (number? v)) ;; skip eids 206 | (or (keyword? v) (lookup-ref? v)) 207 | (try 208 | (d/entid db v) 209 | (catch Exceptions$IllegalArgumentExceptionInfo _ex 210 | nil)))) 211 | 212 | (defn extract-ident-values [db tree] 213 | (->> tree 214 | (tree-seq coll? seq) 215 | (filter (fn [v] (ident-value db v))))) 216 | 217 | (defn ->squashable [& args] 218 | {:squashable (vec args)}) 219 | 220 | (defn squashable? [v] 221 | (and (map? v) (= 1 (count v)) (-> v :squashable vector?))) 222 | 223 | ;; TODO: remove this 224 | (defn db-id->datomic [{:keys [operands]}] 225 | (let [id (first operands) 226 | e-var (gensym-datomic-entity-var)] 227 | (->squashable [(list 'ground id) e-var] 228 | [e-var]))) 229 | 230 | (defn build-datomic-ident-var-map 231 | "Returns a map, keyed by idents with vals `{:eid eid, :var ?id4321}`." 232 | [ident->eid] 233 | (->> ident->eid 234 | (map (fn [[ident eid]] 235 | [ident {:eid eid 236 | :var (gensym-datomic-ident-var)}])) 237 | (into {}))) 238 | 239 | (defn squoosh [cs] 240 | (loop [remain cs, result []] 241 | (if-not (seq remain) 242 | result 243 | (let [[v & vs] remain] 244 | (if (squashable? v) 245 | (recur vs (apply conj result (:squashable v))) 246 | (recur vs (conj result v))))))) 247 | 248 | (defn in->datomic [{:keys [col->var operands]}] 249 | (let [[c vs] operands 250 | attr (table-column->attr-kw c) 251 | {e-sym :entity} (get col->var c :unknown-column!) 252 | clausen (map (fn [v] [e-sym attr v]) vs)] 253 | (util/vec->list (into ['or] clausen)))) 254 | 255 | (defn and->datomic [{:keys [operands] :as env}] 256 | ;; Assumes any toplevel :and in :where has been raised. 257 | ;; Therefore, this should only be called within the scope 258 | ;; of a `or-join`. 259 | (let [env' (dissoc env :op :operands) 260 | dat-clausen (map (partial datomicify-clause env') operands)] 261 | (->> dat-clausen 262 | (into ['and]) 263 | util/vec->list))) 264 | 265 | (defn or->datomic [{:keys [operands] :as env}] 266 | (let [env' (dissoc env :op :operands) 267 | dat-clausen (map (partial datomicify-clause env') operands) 268 | vars (scrape-datomic-vars dat-clausen)] 269 | (->> dat-clausen 270 | (into ['or-join (vec vars)]) 271 | util/vec->list))) 272 | 273 | ;; TODO: This function is very similar to `and->datomic`; refactor? 274 | (defn not->datomic [{:keys [operands] :as env}] 275 | (let [env' (dissoc env :op :operands) 276 | dat-clausen (map (partial datomicify-clause env') operands)] 277 | (->> dat-clausen 278 | (into ['not]) 279 | util/vec->list))) 280 | 281 | (defn tag-clause [clause c] 282 | (vary-meta clause assoc :ir c)) 283 | 284 | (defn build-where-backbone [db clauses] 285 | (let [col->var (->> clauses 286 | extract-columns 287 | build-datomic-var-map) 288 | ident->eid (->> clauses 289 | (tree-seq coll? seq) 290 | (map (fn [n] [n (ident-value db n)])) 291 | (filter second) 292 | (into {})) 293 | ident-env (build-datomic-ident-var-map ident->eid) 294 | base-where (->> col->var 295 | (map (fn [[c {:keys [entity value]}]] 296 | (-> [entity (table-column->attr-kw c) value] 297 | (tag-clause c)))) 298 | (into [])) 299 | ident-where (->> ident-env 300 | (map (fn [[ident {:keys [eid var]}]] 301 | (list 'unify-ident ident var)))) 302 | base-where (apply conj base-where ident-where)] 303 | {:col->var col->var 304 | :ident->eid ident->eid 305 | :ident-env ident-env 306 | :base-where base-where 307 | :ident-where ident-where})) 308 | 309 | (defn datomicify-clause [env clause] 310 | (let [[op & operands] clause 311 | args {:col->var (:col->var env) 312 | :ident-env (:ident-env env) 313 | :op op 314 | :operands operands}] 315 | (case op 316 | :between (between->datomic args) 317 | 318 | (:= :not= :< :> :<= :>=) (binary-comparison->datomic args) 319 | 320 | :db-id (db-id->datomic args) 321 | 322 | :in (in->datomic args) 323 | 324 | :and (and->datomic args) 325 | 326 | :or (or->datomic args) 327 | 328 | :not (not->datomic args) 329 | 330 | (throw (ex-info "unknown where-clause operator" 331 | {:operator op 332 | :operands operands 333 | :clause clause}))))) 334 | 335 | (defn where->datomic [db clauses] 336 | {:pre [(not (empty? clauses)) 337 | (every? list? clauses) 338 | (every? (comp keyword? first) clauses) 339 | (every? (fn [c] 340 | (-> (first c) 341 | #{:between :in 342 | := :not= :< :> :<= :>= 343 | :db-id 344 | :and :or :not})) 345 | clauses)]} 346 | (let [{:keys [col->var ident-env ident-where 347 | base-where]} (build-where-backbone db clauses) 348 | clause-env {:col->var col->var :ident-env ident-env}] 349 | (->> clauses 350 | (map (partial datomicify-clause clause-env)) 351 | squoosh 352 | (into base-where)))) 353 | 354 | (defn scrape-entities [dat-where] 355 | (->> dat-where 356 | (filter (fn [[e]] 357 | (and (symbol? e) 358 | (re-seq #"^\?e\d+" (name e))))) 359 | (map first) 360 | (into #{}))) 361 | 362 | (defn where->datomic-q [db where] 363 | (let [ws (where->datomic db where) 364 | es (scrape-entities ws)] 365 | `[:find ~@es 366 | :in ~'$ ~'% 367 | :where ~@ws])) 368 | 369 | (defn db-id-clause? [clauses] 370 | (some set? clauses)) 371 | 372 | (defn db-id-clause-ir->eids [ir] 373 | (->> ir :where first)) 374 | 375 | (defn fields-ir->attrs [fields] 376 | (->> fields 377 | (map (fn [v] 378 | (if (column? v) 379 | (table-column->attr-kw v) 380 | v))) 381 | (into []))) 382 | 383 | (defn qualified-*-attr? [v] 384 | (and (keyword? v) 385 | (= "*" (name v)) 386 | (seq (namespace v)))) 387 | 388 | (defn gather-attrs-from-entities [entities] 389 | (->> entities (mapcat keys) (into #{}))) 390 | 391 | (defn -resolve-qualified-attrs [qualified-*-kw attrs] 392 | {:pre [(qualified-*-attr? qualified-*-kw) 393 | (coll? attrs) 394 | (every? keyword? attrs)]} 395 | (let [nspace (namespace qualified-*-kw)] 396 | (->> attrs 397 | (map (fn [attr] 398 | {:attr attr :nm (name attr) :ns (namespace attr)})) 399 | (filter (fn [{:keys [ns]}] (= ns nspace))) 400 | (remove (fn [{:keys [nm]}] (= "*" nm))) 401 | (map :attr)))) 402 | 403 | (defn resolve-qualified-attrs [qualified-*-kws attrs] 404 | (let [qualified-*-kws (if (coll? qualified-*-kws) 405 | qualified-*-kws 406 | [qualified-*-kws]) 407 | q*-attrs (->> qualified-*-kws 408 | (filter qualified-*-attr?) 409 | (into #{}))] 410 | (->> q*-attrs 411 | (map (fn [q*] (-resolve-qualified-attrs q* attrs))) 412 | flatten 413 | (into #{})))) 414 | 415 | (defn resolve-attrs [given avail] 416 | (let [univ (into #{} avail)] 417 | ;; preserve given field order 418 | (->> given 419 | (mapcat (fn [attr] 420 | (if (qualified-*-attr? attr) 421 | (-resolve-qualified-attrs attr univ) 422 | [attr]))) 423 | distinct))) 424 | 425 | (defn supplement-with-consts [consts entities] 426 | (let [cs (mapcat (partial repeat 2) consts)] 427 | (for [e entities] 428 | (let [m (into {} e)] 429 | (if (seq cs) 430 | (apply assoc m cs) 431 | m))))) 432 | 433 | (defn update-ir->base-tx-data [{:keys [assign-pairs] :as ir}] 434 | {:pre [(seq assign-pairs) 435 | (vector? assign-pairs) 436 | (every? vector? assign-pairs) 437 | (every? (comp column? first) assign-pairs) 438 | (every? (comp (complement nil?) second) assign-pairs)]} 439 | (->> assign-pairs 440 | (map (fn [[c v]] [(table-column->attr-kw c) v])) 441 | (into {}))) 442 | 443 | (defn stitch-tx-data [base-tx eids] 444 | {:pre [(map? base-tx)]} 445 | (->> eids 446 | (map (fn [id] (assoc base-tx :db/id id))) 447 | (into []))) 448 | 449 | #_(defn get-entities-by-eids [db eids] 450 | (for [eid eids] 451 | (->> eid 452 | (d/entity db) 453 | d/touch))) 454 | 455 | (defn genuine-entity? [entity] 456 | "True iff arg is an entity with attributes. 457 | 458 | `datomic.api/entity` will return an entity object even if the database 459 | had no entity with the given eid. This predicate's purpose is to help 460 | disambiguate the yes-we-found-your-entity results from this other. 461 | " 462 | (and (isa? (class entity) datomic.query.EntityMap) 463 | (not= (->> entity keys (into #{})) 464 | #{}))) 465 | 466 | (defn keep-genuine-entities [entities] 467 | (->> entities 468 | (map d/touch) 469 | (filter genuine-entity?))) 470 | 471 | (defn delete-eids->tx-data [eids] 472 | (->> eids 473 | (map (fn [id] [:db.fn/retractEntity id])) 474 | (into []))) 475 | 476 | (defn add-tempid [entity] 477 | (assoc entity :db/id (d/tempid :db.part/user))) 478 | 479 | (defn insert-ir-traditional->tx-data [{:keys [table cols vals] :as ir}] 480 | {:pre [(seq cols) 481 | (seq vals) 482 | (seq table) 483 | (string? table) 484 | (every? string? cols) 485 | (= (count cols) (count vals))]} 486 | (let [attrs (->> cols (map (partial keyword table)))] 487 | (->> (zipmap attrs vals) 488 | add-tempid 489 | vector))) 490 | 491 | (defn insert-ir-assign-pairs->tx-data [{:keys [assign-pairs] :as ir}] 492 | {:pre [(seq assign-pairs) 493 | (vector? assign-pairs) 494 | (every? vector? assign-pairs) 495 | (every? (comp column? first) assign-pairs) 496 | (every? (comp (complement nil?) second) assign-pairs)]} 497 | (->> assign-pairs 498 | (map (fn [[c v]] [(table-column->attr-kw c) v])) 499 | (into {}) 500 | add-tempid 501 | vector)) 502 | 503 | (defn insert-ir->tx-data [ir] 504 | (if (:assign-pairs ir) 505 | (insert-ir-assign-pairs->tx-data ir) 506 | (insert-ir-traditional->tx-data ir))) 507 | 508 | (defn scrape-inserted-eids [transact-result] 509 | {:pre [(map? transact-result)]} 510 | (->> transact-result :tempids vals (into []))) 511 | 512 | (defn hydrate-entity [db entity-id] 513 | (let [entity (->> entity-id (d/entity db) d/touch)] 514 | (into {:db/id (:db/id entity)} entity))) 515 | 516 | (defn -enumerated-key? [k] 517 | (and (keyword? k) (re-seq #"--#\d+$" (name k)))) 518 | 519 | (defn -decompose-key [k] 520 | (if (-enumerated-key? k) 521 | (if-let [match (re-matches #"^(.*)--#(\d+)$" (name k))] 522 | (let [[_ base-name num] match] 523 | {:name-space (namespace k) 524 | :base-name base-name 525 | :num (Long/parseLong num)}) 526 | (throw (ex-info "unable to split enumerated key" {:key k}))) 527 | {:name-space (namespace k) 528 | :base-name (name k) 529 | :num 1})) 530 | 531 | (defn -enumerate-key [k] 532 | (let [{:keys [name-space base-name num]} (-decompose-key k)] 533 | (keyword name-space (str base-name "--#" (inc num))))) 534 | 535 | (defn -merge-uniq-key [m1 m2] 536 | {:pre [(map? m1) 537 | (map? m2)]} 538 | (let [seen (-> m1 keys set)] 539 | (->> m2 540 | (map (fn [[k v]] 541 | (let [k' (if (seen k) 542 | (->> (iterate -enumerate-key k) 543 | (drop-while seen) 544 | first) 545 | k)] 546 | [k' v]))) 547 | (into m1)))) 548 | 549 | (defn merge-with-enumerated-keys [& ms] 550 | (reduce -merge-uniq-key ms)) 551 | 552 | (defn hydrate-results [db relations] 553 | {:pre [(or (set? relations) 554 | (isa? (class relations) java.util.HashSet)) 555 | (every? vector? relations) 556 | (every? (partial every? integer?) relations)]} 557 | (for [tuple relations] 558 | (let [ent-maps (map (partial hydrate-entity db) tuple)] 559 | ;; Combine each entity in this "row" into a single row. 560 | (if (seq ent-maps) 561 | (apply merge-with-enumerated-keys ent-maps) 562 | {})))) 563 | 564 | ;; Note: `e a v` form is required, including the assumed current value. 565 | ;; @(d/transact conn [[:db/retract 17592186045444 :product/uuid 566 | ;; #uuid "57607426-cdd4-49fa-aecb-0a2572976db9"]]) 567 | ;; Note: If value has changed, then that :db/retract is ignored, but 568 | ;; any other :db/retract's in the given transaction vector 569 | ;; will be carried out. 570 | (defn retract-ir->tx-data 571 | ([db {:keys [ids] :as ir}] 572 | (let [entities (->> (util/get-entities-by-eids db ids) 573 | keep-genuine-entities)] 574 | (retract-ir->tx-data db ir entities))) 575 | 576 | ([db {:keys [attrs] :as ir} entities] 577 | (let [kws (map table-column->attr-kw attrs)] 578 | (->> (for [e entities 579 | kw kws] 580 | (let [eid (:db/id e) 581 | v (get e kw)] 582 | [:db/retract eid kw v])) 583 | (into []))))) 584 | 585 | (comment 586 | 587 | (use 'clojure.repl) 588 | 589 | ;; (def cxn (recreate-default-db)) 590 | 591 | (defn so-touchy [entity] 592 | (walk/prewalk 593 | (fn [n] 594 | (if (= datomic.query.EntityMap (class n)) 595 | (->> n d/touch (into {})) 596 | n)) 597 | entity)) 598 | 599 | (def order2 600 | (->> [:order/orderid 2] 601 | (d/entity (d/db cxn)) 602 | d/touch)) 603 | 604 | (->> order2 so-touchy pp/pprint) 605 | 606 | (def order5 607 | (->> [:order/orderid 5] 608 | (d/entity (d/db cxn)) 609 | d/touch)) 610 | 611 | (->> order5 so-touchy pp/pprint) 612 | 613 | (def sys (.start (system {}))) 614 | (defn sys-cxn [] (->> sys :datomic :connection)) 615 | (def db (d/db (sys-cxn))) 616 | (def bloom (comp d/touch (partial d/entity db))) 617 | (require '[sql-datomic.parser :as par] :reload) 618 | 619 | (def where-clauses 620 | [(list :between {:table "product", :column "price"} 10 15) 621 | (list :not= {:table "product", :column "title"} "AGENT CELEBRITY")]) 622 | 623 | (where->datomic where-clauses) 624 | #_[[?e13699 :product/price ?v13700] [?e13699 :product/title ?v13701] [(clojure.core/<= 10 ?v13700 20)] [(not= ?v13701 "AGENT CELEBRITY")]] 625 | (where->datomic-q where-clauses) 626 | (d/q (where->datomic-q where-clauses) (d/db (sys-cxn))) 627 | (def ids (d/q (where->datomic-q where-clauses) (d/db (sys-cxn)))) 628 | (mapcat identity ids) 629 | (->> ids 630 | (mapcat identity) 631 | (map (fn [id] 632 | (->> id 633 | (d/entity (d/db (sys-cxn))) 634 | d/touch)))) 635 | 636 | (where->datomic 637 | [(list := {:table "product" :column "prod-id"} 42)]) 638 | 639 | (->> (d/q '[:find [?e ...] 640 | :in $ 641 | :where 642 | [(ground 17592186045445) ?e] 643 | [?e]] 644 | db) 645 | (map bloom) ) 646 | 647 | (def target (->> (d/q '[:find [?e ...] 648 | :in $ 649 | :where 650 | [?e :product/prod-id 1567]] 651 | db) 652 | (map bloom) 653 | first)) 654 | @(d/transact (sys-cxn) [{:db/id (:db/id target) 655 | :product/tag :ace-meet-greet-feet-beet-leet 656 | :product/special true}]) 657 | (def db (d/db (sys-cxn))) 658 | (def bloom (comp d/touch (partial d/entity db))) 659 | (->> (d/q '[:find [?e ...] 660 | :in $ 661 | :where 662 | [?e :product/prod-id 1567]] 663 | db) 664 | (map bloom) 665 | first 666 | (into {}) 667 | pp/pprint) 668 | 669 | [:set_clausen 670 | [:assignment_pair 671 | {:table "product", :column "category"} 672 | :product.category/new]] 673 | 674 | (->> (d/q '[:find 675 | [?e3160 ...] 676 | :in 677 | $ 678 | % 679 | :where 680 | [?e3160 :product/prod-id ?v3161] 681 | [?e3160 :product/category ?v3162] 682 | (unify-ident :product.category/action ?ident3163) 683 | [(> ?v3161 1000)] 684 | [(= ?v3162 ?ident3163)]] 685 | db rules) 686 | (map bloom) 687 | (map (juxt :product/prod-id :product/category)) 688 | sort) 689 | (->> (d/q '[:find 690 | [?e3160 ...] 691 | :in 692 | $ 693 | % 694 | :where 695 | [?e3160 :product/prod-id ?v3161] 696 | [?e3160 :product/category ?v3162] 697 | (unify-ident :product.category/action ?ident3163) 698 | (or-join [?v3161 ?v3162 ?ident3163] 699 | [(< ?v3161 5000)] 700 | [(= ?v3162 ?ident3163)])] 701 | db rules) 702 | (map bloom) 703 | (map (juxt :product/prod-id :product/category)) 704 | sort) 705 | ;; If used with just a plain `or`, we get this error: 706 | ;; AssertionError Assert failed: All clauses in 'or' must use same set of vars, had [#{?v3161} #{?v3162 ?ident3163}] 707 | ;; If used with `or-join`, then seems to work. 708 | ;; Implies the need to scrape `or-join` clauses for `?vars`. 709 | 710 | ;; Regarding `not` vs `not-join`, `not` does not seem to have the 711 | ;; same limitations re binding as does `or`. In other words, this: 712 | ;; (not (or-join [?v3161 ?v3162 ?ident3163] 713 | ;; [(< ?v3161 5000)] 714 | ;; [(= ?v3162 ?ident3163)])) 715 | ;; is just fine. Using `not-join` with the same bindings as 716 | ;; `or-join` also works but seems to be unnecessary. 717 | 718 | ) 719 | -------------------------------------------------------------------------------- /src/sql_datomic/delete_command.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.delete-command 2 | (:require [sql-datomic.datomic :as dat] 3 | [sql-datomic.util :as util :refer [squawk]] 4 | [datomic.api :as d] 5 | [clojure.pprint :as pp])) 6 | 7 | (defn -run-harness [{:keys [conn db ir options ids]}] 8 | (let [{:keys [debug pretend silent]} options 9 | entities (->> (util/get-entities-by-eids db ids) 10 | dat/keep-genuine-entities)] 11 | (when debug (squawk "Entities Targeted for Delete")) 12 | ;; Always give indication of what will be deleted. 13 | (when-not silent 14 | (println (if (seq entities) ids "None"))) 15 | (when debug (util/-debug-display-entities entities)) 16 | 17 | (if-not (seq entities) 18 | {:ids ids 19 | :entities entities} 20 | 21 | ;; else 22 | (let [tx-data (dat/delete-eids->tx-data ids)] 23 | (when debug (squawk "Transaction" tx-data)) 24 | (if pretend 25 | (do 26 | (println "Halting transaction due to pretend mode ON") 27 | {:ids ids 28 | :entities entities 29 | :pretend pretend 30 | :tx-data tx-data}) 31 | 32 | ;; else 33 | (let [result @(d/transact conn tx-data)] 34 | (when-not silent 35 | (println) 36 | (println result)) 37 | (when debug 38 | (squawk "Entities after Transaction") 39 | (util/-debug-display-entities-by-ids (:db-after result) ids)) 40 | {:ids ids 41 | :entities entities 42 | :tx-data tx-data 43 | :result result})))))) 44 | 45 | (defn -run-db-id-delete 46 | [conn db ir opts] 47 | (let [ids (dat/db-id-clause-ir->eids ir)] 48 | (-run-harness {:conn conn 49 | :db db 50 | :ir ir 51 | :options opts 52 | :ids ids}))) 53 | 54 | (defn -run-normal-delete 55 | [conn db {:keys [where table] :as ir} {:keys [debug] :as opts}] 56 | (let [query (dat/where->datomic-q db where)] 57 | (when debug 58 | (squawk "Datomic Rules" dat/rules) 59 | (squawk "Datomic Query" query)) 60 | 61 | (let [results (d/q query db dat/rules)] 62 | (when debug (squawk "Raw Results" results)) 63 | (cond 64 | (or (->> results seq not) 65 | (->> results first count (= 1))) 66 | (let [ids (mapcat identity results)] 67 | (-run-harness {:conn conn 68 | :db db 69 | :ir ir 70 | :options opts 71 | :ids ids})) 72 | 73 | ;; Assertion: results is non-empty 74 | ;; Assertion: tuples in results have arity > 1 75 | (seq table) 76 | (let [i (util/infer-entity-index ir query) 77 | ids (map (fn [row] (nth row i)) results)] 78 | (when debug 79 | (binding [*out* *err*] 80 | (printf "Narrowing entity vars to index: %d\n" i) 81 | (prn [:ids ids]))) 82 | (-run-harness {:conn conn 83 | :db db 84 | :ir ir 85 | :options opts 86 | :ids ids})) 87 | 88 | ;; Assertion: no given table 89 | :else 90 | (throw 91 | (IllegalArgumentException. 92 | "\n!!! Missing delete target. Necessary with joins. Aborting delete !!!\n")))))) 93 | 94 | (defn run-delete 95 | ([conn db ir] (run-delete conn db ir {})) 96 | ([conn db {:keys [where] :as ir} opts] 97 | {:pre [(= :delete (:type ir))]} 98 | (when (seq where) 99 | (if (dat/db-id-clause? where) 100 | (-run-db-id-delete conn db ir opts) 101 | (-run-normal-delete conn db ir opts))))) 102 | -------------------------------------------------------------------------------- /src/sql_datomic/insert_command.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.insert-command 2 | (:require [sql-datomic.datomic :as dat] 3 | [sql-datomic.util :as util :refer [squawk]] 4 | [datomic.api :as d] 5 | [clojure.pprint :as pp])) 6 | 7 | (defn run-insert 8 | ([conn ir] (run-insert conn ir {})) 9 | ([conn ir {:keys [debug pretend silent] :as opts}] 10 | {:pre [(= :insert (:type ir))]} 11 | 12 | (let [tx-data (dat/insert-ir->tx-data ir)] 13 | (when debug (squawk "Transaction" tx-data)) 14 | (if pretend 15 | (do 16 | (println "Halting transaction due to pretend mode ON") 17 | {:tx-data tx-data 18 | :pretend pretend}) 19 | (let [result @(d/transact conn tx-data) 20 | ids (dat/scrape-inserted-eids result)] 21 | (when-not silent 22 | (println) 23 | (prn ids)) 24 | (when debug 25 | (squawk "Entities after Transaction") 26 | (util/-debug-display-entities-by-ids (:db-after result) ids)) 27 | {:tx-data tx-data 28 | :ids ids 29 | :result result}))))) 30 | -------------------------------------------------------------------------------- /src/sql_datomic/parser.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.parser 2 | (:require [instaparse.core :as insta] 3 | [clojure.zip :as zip] 4 | [clojure.string :as str] 5 | [clojure.set :as set] 6 | [clojure.edn :as edn] 7 | [clj-time.format :as fmt] 8 | [clj-time.core :as tm] 9 | [clj-time.coerce :as coer] 10 | [clojure.instant :as inst] 11 | [sql-datomic.types :as types] 12 | [sql-datomic.util :as util] 13 | [clojure.walk :as walk])) 14 | 15 | (def parser 16 | (-> "resources/sql-eensy.bnf" 17 | slurp 18 | (insta/parser 19 | :input-format :ebnf 20 | :no-slurp true 21 | :string-ci true 22 | :auto-whitespace :standard))) 23 | 24 | (def good-ast? (complement insta/failure?)) 25 | 26 | (def reserved #{"and" "or" "select" "where" "not" "insert" "update" 27 | "set" "delete" "from" "into" "in" "between"}) 28 | 29 | (defn strip-doublequotes [s] 30 | (-> s 31 | (str/replace #"^\"" "") 32 | (str/replace #"\"$" ""))) 33 | 34 | (defn transform-string-literal [s] 35 | (-> s 36 | (str/replace #"^'" "") 37 | (str/replace #"'$" "") 38 | (str/replace #"\\'" "'"))) 39 | 40 | (def comparison-ops 41 | {"=" := 42 | "<>" :not= 43 | "!=" :not= 44 | "<" :< 45 | "<=" :<= 46 | ">" :> 47 | ">=" :>=}) 48 | 49 | (def datetime-formatter (fmt/formatters :date-hour-minute-second)) 50 | (def date-formatter (fmt/formatters :date)) 51 | 52 | (defn to-utc [d] 53 | (tm/from-time-zone d tm/utc)) 54 | 55 | (defn fix-datetime-separator-space->T [s] 56 | (str/replace s #"^(.{10}) (.*)$" "$1T$2")) 57 | 58 | (defn transform-datetime-literal [s] 59 | (->> s 60 | fix-datetime-separator-space->T 61 | (fmt/parse datetime-formatter) 62 | to-utc 63 | str 64 | inst/read-instant-date)) 65 | 66 | (defn transform-date-literal [s] 67 | (->> s 68 | (fmt/parse date-formatter) 69 | to-utc 70 | str 71 | inst/read-instant-date)) 72 | 73 | (defn transform-epochal-literal [s] 74 | (->> s 75 | Long/parseLong 76 | (*' 1000) ;; ->milliseconds 77 | coer/from-long 78 | str 79 | inst/read-instant-date)) 80 | 81 | (defn transform-float-literal [s] 82 | (let [s' (if (->> s last #{\F \f}) 83 | (subs s 0 (-> s count dec)) 84 | s)] 85 | (Float/parseFloat s'))) 86 | 87 | (defn reducible-or-tree [& vs] 88 | (case (count vs) 89 | 1 (first vs) 90 | (util/vec->list (into [:or] vs)))) 91 | 92 | (defn reducible-and-tree [& vs] 93 | (case (count vs) 94 | 1 (first vs) 95 | (util/vec->list (into [:and] vs)))) 96 | 97 | (defn translate-boolean-negative [& vs] 98 | ;; TODO: reduce nested nots 99 | (util/vec->list (into [:not] vs))) 100 | 101 | (defn flatten-nested-logical-connectives-1 102 | "Flattens left-leaning `:and` or `:or` into single level. 103 | 104 | (:and (:and :foo :bar) :baz) => (:and :foo :bar :baz) 105 | (:or (:or :foo :bar) :baz) => (:or :foo :bar :baz) 106 | " 107 | [tree] 108 | (let [and-list? (fn [n] 109 | (and (list? n) (= :and (first n)))) 110 | nested-and-list? (fn [a-list] 111 | (and (and-list? a-list) 112 | (and-list? (second a-list)))) 113 | or-list? (fn [n] 114 | (and (list? n) (= :or (first n)))) 115 | nested-or-list? (fn [a-list] 116 | (and (or-list? a-list) 117 | (or-list? (second a-list)))) 118 | lift (fn [connective n] 119 | (let [[_con [_con & targets] & vs] n] 120 | (-> [connective] 121 | (into targets) 122 | (into vs) 123 | util/vec->list))) 124 | lift-and (partial lift :and) 125 | lift-or (partial lift :or)] 126 | (walk/prewalk 127 | (fn [n] 128 | (cond 129 | (nested-and-list? n) (lift-and n) 130 | (nested-or-list? n) (lift-or n) 131 | :else n)) 132 | tree))) 133 | 134 | (defn flatten-nested-logical-connectives [tree] 135 | (->> 136 | (iterate (fn [[_ v]] 137 | [v (flatten-nested-logical-connectives-1 v)]) 138 | [nil tree]) 139 | (take-while (fn [[old new]] (not= old new))) 140 | last 141 | last)) 142 | 143 | (defn raise-toplevel-and [ir] 144 | (if (and (->> ir :where vector?) 145 | (->> ir :where count #{1}) 146 | (->> ir :where first list?) 147 | (->> ir :where first first #{:and}) 148 | (> (->> ir :where first count) 1)) 149 | (let [and-args (-> ir (get-in [:where 0]) pop)] 150 | (assoc ir :where (vec and-args))) 151 | ir)) 152 | 153 | (defn tag-type [obj type] 154 | (vary-meta obj assoc :ast-type type)) 155 | 156 | (defn by-tag-type [tagged-vs] 157 | (->> tagged-vs 158 | (group-by (comp :ast-type meta)) 159 | (map (fn [[k v]] 160 | [k (first v)])) 161 | (into {}))) 162 | 163 | (defn wrap-with-tag-type [f type] 164 | (fn [& vs] 165 | (tag-type (apply f vs) type))) 166 | 167 | (defn -unpack-table [m] 168 | (if (:table m) (update m :table first) m)) 169 | 170 | (def transform-options 171 | {:sql_data_statement identity 172 | :select_statement (fn [& ps] 173 | (-> ps 174 | by-tag-type 175 | (set/rename-keys {:select_list :fields 176 | :from_clause :tables 177 | :where_clause :where}) 178 | (assoc :type :select))) 179 | :update_statement (fn [& ps] 180 | (-> ps 181 | by-tag-type 182 | (set/rename-keys {:table_name :table 183 | :set_clausen :assign-pairs 184 | :where_clause :where}) 185 | -unpack-table 186 | (assoc :type :update))) 187 | :insert_statement (fn [& ps] 188 | (-> ps 189 | by-tag-type 190 | (set/rename-keys {:table_name :table 191 | :insert_cols :cols 192 | :insert_vals :vals 193 | :set_clausen :assign-pairs}) 194 | -unpack-table 195 | (assoc :type :insert))) 196 | :delete_statement (fn [& ps] 197 | (-> ps 198 | by-tag-type 199 | (set/rename-keys {:table_name :table 200 | :where_clause :where}) 201 | -unpack-table 202 | (assoc :type :delete))) 203 | :retract_statement (fn [& ps] 204 | (let [[attrs ids] ps] 205 | {:type :retract 206 | :ids ids 207 | :attrs attrs})) 208 | :select_list (wrap-with-tag-type vector :select_list) 209 | :column_name (fn [t c] {:table (strip-doublequotes t) 210 | :column (strip-doublequotes c)}) 211 | :string_literal transform-string-literal 212 | :long_literal edn/read-string 213 | :bigint_literal edn/read-string 214 | :float_literal transform-float-literal 215 | :double_literal edn/read-string 216 | :bigdec_literal edn/read-string 217 | :keyword_literal keyword 218 | :boolean_literal identity 219 | :true (constantly true) 220 | :false (constantly false) 221 | :from_clause (wrap-with-tag-type vector :from_clause) 222 | :table_ref (fn [[name] & [alias]] 223 | (cond-> {:name name} 224 | alias (assoc :alias alias))) 225 | :where_clause (wrap-with-tag-type vector :where_clause) 226 | :search_condition reducible-or-tree 227 | :db_id_clause (fn [& eids] (into #{} eids)) 228 | :boolean_term reducible-and-tree 229 | :boolean_factor identity 230 | :boolean_negative translate-boolean-negative 231 | :boolean_test identity 232 | :boolean_primary identity 233 | :table_name (wrap-with-tag-type 234 | ;; cannot hang metadata on string, so wrap in vector 235 | (comp vector strip-doublequotes) :table_name) 236 | :table_alias strip-doublequotes 237 | :binary_comparison (fn [c op v] 238 | (list (comparison-ops op) c v)) 239 | :between_clause (fn [c v1 v2] 240 | (list :between c v1 v2)) 241 | :date_literal transform-date-literal 242 | :datetime_literal transform-datetime-literal 243 | :epochal_literal transform-epochal-literal 244 | :inst_literal inst/read-instant-date 245 | :uuid_literal (fn [s] (java.util.UUID/fromString s)) 246 | :uri_literal types/->uri 247 | :bytes_literal types/base64-str->bytes 248 | :set_clausen (wrap-with-tag-type vector :set_clausen) 249 | :assignment_pair (wrap-with-tag-type vector :assignment_pair) 250 | :retract_clausen (wrap-with-tag-type vector :retract_clausen) 251 | :retract_attrs vector 252 | :insert_cols (wrap-with-tag-type vector :insert_cols) 253 | :insert_vals (wrap-with-tag-type vector :insert_vals) 254 | :in_clause (fn [c & vs] 255 | (list :in c (into [] vs))) 256 | :qualified_asterisk (fn [s] (keyword s "*"))}) 257 | 258 | (defn transform [ast] 259 | (->> ast 260 | (insta/transform transform-options) 261 | flatten-nested-logical-connectives 262 | raise-toplevel-and)) 263 | 264 | (defn parse [input] 265 | (->> (parser input) 266 | transform)) 267 | 268 | (defn seems-to-mix-db-id-in-where? [sql-text] 269 | (let [s (str/lower-case sql-text)] 270 | (if-let [s' (re-seq #"\bwhere\b.+$" s)] 271 | (let [tokens (str/split (first s') #"\s+") 272 | m (->> tokens 273 | (mapcat #(str/split % #"(?:<=|>=|=|!=|<>|>|<|[()]+)")) 274 | (filter seq) 275 | (remove reserved) 276 | (filter #(re-seq #"^[-a-z_:]" %)) 277 | (group-by #(if (#{":db/id" "db.id"} %) :db-id :other)))] 278 | (if (and (-> m :db-id seq) 279 | (-> m :other seq)) 280 | m ; more useful true value 281 | false)) 282 | false))) 283 | 284 | (defn hint-for-parse-error [parse-error] 285 | (let [{:keys [index reason line column text]} parse-error 286 | c (get text index)] 287 | (cond 288 | (or (= c \") 289 | (re-seq #"(?i)\bwhere\s+.*[^<>](?:=|!=|<>)\s*\"" text)) 290 | "Did you use \" for string literal? Strings are delimited by '." 291 | 292 | (and 293 | (re-seq #"^(?:#bytes|#base64)" (subs text index)) 294 | (re-seq 295 | #"(?i)\bwhere\s+.*(?:=|!=|<>|<=|<|>=|>)\s*(?:#bytes|#base64)\b" 296 | text)) 297 | (str "Did you use a #bytes literal in a comparison? " 298 | "Bytes arrays are not values" 299 | " and cannot be used with =, !=, <>, <, etc.") 300 | 301 | (and (= c \:) 302 | (some (fn [{:keys [tag expecting]}] 303 | (and (= tag :string) 304 | "#attr")) 305 | reason)) 306 | (str "Expecting a column name. " 307 | "Did you forget to use #attr on a keyword?") 308 | 309 | (seems-to-mix-db-id-in-where? text) 310 | (str "#attr :db/id cannot be mixed with regular attrs/columns" 311 | " in WHERE clauses.") 312 | 313 | :else nil))) 314 | -------------------------------------------------------------------------------- /src/sql_datomic/repl.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.repl 2 | (:require [sql-datomic.parser :as par] 3 | [sql-datomic.datomic :as dat] 4 | [sql-datomic.util :as util :refer [squawk]] 5 | [sql-datomic.select-command :as sel] 6 | [sql-datomic.update-command :as upd] 7 | [sql-datomic.delete-command :as del] 8 | [sql-datomic.insert-command :as ins] 9 | [sql-datomic.retract-command :as rtr] 10 | [clojure.pprint :as pp] 11 | [clojure.tools.cli :as cli] 12 | [datomic.api :as d] 13 | clojure.repl 14 | [clojure.string :as str] 15 | [clojure.set :as set] 16 | [sql-datomic.tabula :as tab] 17 | [sql-datomic.schema :as sch]) 18 | ;; (:gen-class) 19 | ) 20 | 21 | (def ^:dynamic *prompt* "sql> ") 22 | 23 | (declare sys) 24 | 25 | (defn pointer [s index] 26 | (str/join (conj (into [] (repeat (dec index) \space)) \^))) 27 | 28 | (defn ruler [s] 29 | (let [nats (->> (range) (map inc) (take (count s))) 30 | ->line (fn [coll] 31 | (->> coll (map str) str/join)) 32 | ones (map (fn [n] (rem n 10)) 33 | nats) 34 | tens (map (fn [n] (if (zero? (rem n 10)) 35 | (rem (quot n 10) 10) 36 | \space)) 37 | nats)] 38 | (str/join "\n" [(->line ones) (->line tens)]))) 39 | 40 | (defn print-ruler 41 | ([input] (print-ruler input nil)) 42 | ([input index] 43 | (when (seq input) 44 | (binding [*out* *err*] 45 | (println "\nInput with column offsets:\n==========================") 46 | (println input) 47 | (when index 48 | (println (pointer input index))) 49 | (println (ruler input)) 50 | (flush))))) 51 | 52 | (defn show-tables [] 53 | (let [db (->> sys :datomic :connection d/db) 54 | summary (sch/summarize-schema db) 55 | table-names (->> summary :tables keys sort)] 56 | (doseq [t table-names] 57 | (println t))) 58 | (flush)) 59 | 60 | (defn -show-enums [es] 61 | (when (seq es) 62 | (println "Related Enums") 63 | (let [by-ns (group-by namespace es)] 64 | (doseq [k (-> by-ns keys sort)] 65 | (println (str/join " " (get by-ns k))))))) 66 | 67 | (defn describe-table [name] 68 | (let [db (->> sys :datomic :connection d/db) 69 | summary (sch/summarize-schema db) 70 | table (get-in summary [:tables name]) 71 | enums (get-in summary [:enums name])] 72 | (if-not (seq table) 73 | (println "Unknown table") 74 | 75 | (do 76 | (pp/print-table [:db/ident :db/valueType :db/cardinality 77 | :db/unique :db/doc] 78 | table) 79 | (-show-enums enums))) 80 | (flush))) 81 | 82 | (defn describe-entity [eid] 83 | (let [db (->> sys :datomic :connection d/db) 84 | e (sch/eid->map db eid) 85 | s (sch/infer-schema-of-entity db e)] 86 | (if-not (seq s) 87 | (println "Unable to find schema") 88 | (pp/print-table [:db/ident :db/valueType :db/cardinality 89 | :db/unique :db/doc] 90 | s)) 91 | (flush))) 92 | 93 | (defn show-schema [] 94 | (let [db (->> sys :datomic :connection d/db) 95 | summary (sch/summarize-schema db) 96 | table-map (get-in summary [:tables]) 97 | ts (->> table-map (mapcat second) (sort-by :db/ident)) 98 | enums-map (get-in summary [:enums]) 99 | es (->> enums-map (mapcat second) (sort-by :db/ident))] 100 | (pp/print-table [:db/ident :db/valueType :db/cardinality 101 | :db/unique :db/doc] 102 | ts) 103 | (-show-enums es) 104 | (flush))) 105 | 106 | (defn show-status [m] 107 | (let [m' (dissoc m :help) 108 | ks (-> m' keys sort) 109 | pks (map name ks) 110 | k-max-len (->> pks 111 | (map (comp count str)) 112 | (sort >) 113 | first) 114 | row-fmt (str "%-" k-max-len "s : %s\n")] 115 | (doseq [[k pk] (map vector ks pks)] 116 | (let [v (get m' k) 117 | v' (if (instance? Boolean v) 118 | (if v "ON" "OFF") 119 | v)] 120 | (printf row-fmt pk v')))) 121 | (flush)) 122 | 123 | (defn print-help [] 124 | (println "type `exit` or `quit` or ^D to exit") 125 | (println "type `debug` to toggle debug mode") 126 | (println "type `pretend` to toggle pretend mode") 127 | (println "type `expanded` or `\\x` to toggle expanded display mode") 128 | (println "type `show tables` or `\\d` to show Datomic \"tables\"") 129 | (println "type `show schema` or `\\dn` to show all user Datomic schema") 130 | (println "type `describe $table` or `\\d $table` to describe a Datomic \"table\"") 131 | (println "type `describe $dbid` or `\\d $dbid` to describe the schema of an entity") 132 | (println "type `status` to show toggle values, conn strings, etc.") 133 | (println "type `\\?`, `?`, `h` or `help` to see this listing") 134 | (flush)) 135 | 136 | (defn repl [{:keys [debug pretend expanded] :as opts}] 137 | (let [dbg (atom debug) 138 | loljk (atom pretend) 139 | x-flag (atom expanded) 140 | noop (atom false)] 141 | (print *prompt*) 142 | (flush) 143 | (let [input (read-line)] 144 | (when-not input 145 | (System/exit 0)) 146 | (when (re-seq #"^(?ims)\s*(?:quit|exit)\s*$" input) 147 | (System/exit 0)) 148 | 149 | (try 150 | (when (re-seq #"^(?i)\s*debug\s*$" input) 151 | (let [new-debug (not @dbg)] 152 | (println "Set debug to" (if new-debug "ON" "OFF")) 153 | (flush) 154 | (reset! dbg new-debug) 155 | (reset! noop true))) 156 | (when (re-seq #"^(?i)\s*pretend\s*$" input) 157 | (let [new-pretend (not @loljk)] 158 | (println "Set pretend to" (if new-pretend "ON" "OFF")) 159 | (flush) 160 | (reset! loljk new-pretend) 161 | (reset! noop true))) 162 | (when (and (not @dbg) @loljk) 163 | (println "Set debug to ON due to pretend ON") 164 | (flush) 165 | (reset! dbg true) 166 | (reset! noop true)) 167 | (when (re-seq #"^(?i)\s*(?:\\x|expanded)\s*$" input) 168 | (let [new-expand (not @x-flag)] 169 | (println "Set expanded display to" (if new-expand "ON" "OFF")) 170 | (flush) 171 | (reset! x-flag new-expand) 172 | (reset! noop true))) 173 | (when (re-seq #"^(?i)\s*(?:h|help|\?+|\\\?)\s*$" input) 174 | (print-help) 175 | (reset! noop true)) 176 | (when (re-seq #"^(?i)\s*(?:show\s+tables|\\d)\s*$" input) 177 | (show-tables) 178 | (reset! noop true)) 179 | (when (re-seq #"^(?i)\s*(?:show\s+schema|\\dn)\s*$" input) 180 | (show-schema) 181 | (reset! noop true)) 182 | (when-let [match (re-matches #"^(?i)\s*(?:desc(?:ribe)?|\\d)\s+(\S+)\s*$" input)] 183 | (let [[_ x] match] 184 | (if (re-seq #"^\d+$" x) 185 | (describe-entity (Long/parseLong x)) 186 | (describe-table x)) 187 | (reset! noop true))) 188 | (when (re-seq #"^(?i)\s*status\s*$" input) 189 | (show-status opts) 190 | (reset! noop true)) 191 | 192 | (when (and (not @noop) (re-seq #"(?ms)\S" input)) 193 | (let [maybe-ast (par/parser input)] 194 | (if-not (par/good-ast? maybe-ast) 195 | (do 196 | (squawk "Parse error" maybe-ast) 197 | (when-let [hint (par/hint-for-parse-error maybe-ast)] 198 | (binding [*out* *err*] 199 | (println (str "\n*** Hint: " hint)))) 200 | (print-ruler input (:column maybe-ast))) 201 | (do 202 | (when @dbg (squawk "AST" maybe-ast)) 203 | (let [conn (->> sys :datomic :connection) 204 | db (d/db conn) 205 | ir (par/transform maybe-ast) 206 | opts' {:debug @dbg :pretend @loljk}] 207 | (when @dbg (squawk "Intermediate Repr" ir)) 208 | 209 | (case (:type ir) 210 | 211 | :select 212 | (let [result (sel/run-select db ir opts') 213 | entities (:entities result) 214 | attrs (:attrs result) 215 | print-table-fn (if @x-flag 216 | tab/print-expanded-table 217 | tab/print-simple-table)] 218 | (print-table-fn (seq attrs) entities) 219 | (flush)) 220 | 221 | :update 222 | (upd/run-update conn db ir opts') 223 | 224 | :delete 225 | (del/run-delete conn db ir opts') 226 | 227 | :insert 228 | (ins/run-insert conn ir opts') 229 | 230 | :retract 231 | (rtr/run-retract conn db ir opts') 232 | 233 | ;; else 234 | (throw (ex-info "Unknown query type" {:type (:type ir) 235 | :ir ir})))))))) 236 | (catch Exception ex 237 | (binding [*out* *err*] 238 | (println "\n!!! Error !!!") 239 | (if @dbg 240 | (do 241 | (clojure.repl/pst ex) 242 | (print-ruler input)) 243 | (println (.toString ex))) 244 | (flush)))) 245 | 246 | (recur (assoc opts 247 | :debug @dbg 248 | :pretend @loljk 249 | :expanded @x-flag))))) 250 | 251 | (defn -main [& args] 252 | (let [[opts args banner] 253 | (cli/cli args 254 | ["-h" "--help" "Print this help" 255 | :flag true 256 | :default false] 257 | ["-d" "--debug" "Write debug info to stderr" 258 | :flag true 259 | :default false] 260 | ["-p" "--pretend" "Run without transacting; turns on debug" 261 | :flag true 262 | :default false] 263 | ["-x" "--expanded" 264 | "Display resultsets in expanded output format" 265 | :flag true 266 | :default false] 267 | ["-u" "--connection-uri" 268 | "URI to Datomic DB; if missing, uses default mem db"] 269 | ["-s" "--default-schema-name" 270 | ":dellstore or :starfighter or :seattle, for default in-mem db" 271 | :parse-fn (fn [s] 272 | (if-let [m (re-matches #"^:+(.+)$" s)] 273 | (keyword (second m)) 274 | (keyword s))) 275 | :default :dellstore])] 276 | (when (:help opts) 277 | (println banner) 278 | (System/exit 0)) 279 | 280 | (let [opts' (set/rename-keys opts {:default-schema-name :schema-name})] 281 | 282 | (def sys (.start (dat/system opts'))) 283 | 284 | (let [uri (->> sys :datomic :connection-uri)] 285 | (when (dat/default-uri? uri) 286 | (println "*** using default in-mem database ***")) 287 | (print-help) 288 | (repl (assoc opts' 289 | :connection-uri uri)))))) 290 | 291 | (comment 292 | 293 | (def sys (.start (dat/system {}))) 294 | (def conn (->> sys :datomic :connection)) 295 | (def db (d/db conn)) 296 | 297 | (let [stmt "select where product.prod-id = 9990" 298 | ir (->> stmt par/parser par/transform)] 299 | (->> 300 | (sel/run-select db ir #_{:debug true}) 301 | :entities 302 | pp/pprint)) 303 | 304 | (let [stmt "update product.rating = 3.14f where product.prod-id > 8000" 305 | ir (->> stmt par/parser par/transform)] 306 | (->> 307 | (upd/run-update conn db ir #_{:debug true :pretend nil}) 308 | pp/pprint) 309 | (->> (d/q '[:find [?e ...] 310 | :where 311 | [?e :product/prod-id ?pid] 312 | [(> ?pid 8000)]] 313 | (d/db conn)) 314 | (map #(d/touch (d/entity (d/db conn) %))) 315 | (map #(into {} %)) 316 | pp/pprint)) 317 | 318 | (let [stmt "delete where order.orderid = 2" 319 | ir (->> stmt par/parser par/transform)] 320 | (->> (d/q '[:find ?e ?oid 321 | :where 322 | [?e :order/orderid ?oid]] 323 | (d/db conn)) 324 | pp/pprint) 325 | (->> 326 | (del/run-delete conn db ir #_{:debug true :pretend nil}) 327 | #_pp/pprint) 328 | (->> (d/q '[:find ?e ?oid 329 | :where 330 | [?e :order/orderid ?oid]] 331 | (d/db conn)) 332 | pp/pprint)) 333 | 334 | (let [stmt "insert customer.customerid = 1234, 335 | customer.email = 'foo@example.com'" 336 | ir (->> stmt par/parser par/transform)] 337 | (->> (d/q '[:find ?e ?cid 338 | :where 339 | [?e :customer/customerid ?cid]] 340 | (d/db conn)) 341 | pp/pprint) 342 | (->> 343 | (ins/run-insert conn ir #_{:debug true :pretend nil}) 344 | pp/pprint) 345 | (->> (d/q '[:find ?e ?cid 346 | :where 347 | [?e :customer/customerid ?cid]] 348 | (d/db conn)) 349 | pp/pprint)) 350 | 351 | ) 352 | -------------------------------------------------------------------------------- /src/sql_datomic/retract_command.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.retract-command 2 | (:require [sql-datomic.datomic :as dat] 3 | [sql-datomic.util :as util 4 | :refer [squawk -debug-display-entities]] 5 | [datomic.api :as d] 6 | [clojure.pprint :as pp])) 7 | 8 | (defn -run-harness [{:keys [conn db ir options ids]}] 9 | (let [{:keys [debug pretend silent]} options 10 | {:keys [attrs]} ir 11 | entities (->> (util/get-entities-by-eids db ids) 12 | dat/keep-genuine-entities)] 13 | (when debug (squawk "Entities Targeted for Attr Retraction")) 14 | (when-not silent 15 | (println (if (seq entities) ids "None"))) 16 | (when debug (-debug-display-entities entities)) 17 | 18 | (if-not (seq entities) 19 | {:ids ids 20 | :before entities 21 | :pretend pretend} 22 | 23 | (let [tx-data (dat/retract-ir->tx-data db ir entities)] 24 | (when debug (squawk "Transaction" tx-data)) 25 | (if pretend 26 | (do 27 | (println "Halting transaction due to pretend mode ON") 28 | {:ids ids 29 | :before entities 30 | :tx-data tx-data 31 | :pretend pretend}) 32 | 33 | (let [result @(d/transact conn tx-data)] 34 | (when-not silent 35 | (println) 36 | (println result)) 37 | (let [entities' (util/get-entities-by-eids (:db-after result) ids)] 38 | (when debug 39 | (squawk "Entities after Transaction") 40 | (-debug-display-entities entities')) 41 | {:ids ids 42 | :tx-data tx-data 43 | :before entities 44 | :after entities' 45 | :result result}))))))) 46 | 47 | (defn -run-db-id-retract 48 | [conn db {:keys [ids] :as ir} opts] 49 | (-run-harness {:conn conn 50 | :db db 51 | :ir ir 52 | :options opts 53 | :ids ids})) 54 | 55 | ;; {:type :retract, 56 | ;; :ids #{1234 42}, 57 | ;; :attrs 58 | ;; [{:table "product", :column "category"} 59 | ;; {:table "product", :column "uuid"}]} 60 | 61 | (defn run-retract 62 | ([conn db ir] (run-retract conn db ir {})) 63 | ([conn db {:keys [ids attrs] :as ir} opts] 64 | {:pre [(= :retract (:type ir))]} 65 | (when (seq ids) 66 | (-run-db-id-retract conn db ir opts)))) 67 | -------------------------------------------------------------------------------- /src/sql_datomic/schema.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.schema 2 | (:require [datomic.api :as d] 3 | [clojure.pprint :as pp])) 4 | 5 | ;; Blacklist of keyword namespaces to elide when looking at attrs. 6 | (def system-ns #{ ;; s/ => untangled.datomic.schema 7 | "confirmity" ;; conformity migration lib 8 | "constraint" ;; s/reference-constraint-for-attribute 9 | "datomic-toolbox" ;; from lib of same name 10 | "db" 11 | "db.alter" 12 | "db.bootstrap" 13 | "db.cardinality" 14 | "db.excise" 15 | "db.fn" 16 | "db.install" 17 | "db.lang" 18 | "db.part" 19 | "db.sys" 20 | "db.type" 21 | "db.unique" 22 | "entity" ;; s/entity-extensions 23 | "fressian" 24 | }) 25 | 26 | (defn get-user-schema 27 | "Returns seq of [eid attr-kw] pairs." 28 | [db] 29 | (d/q '[:find ?e ?ident 30 | :in $ ?system-ns 31 | :where 32 | [?e :db/ident ?ident] 33 | [(namespace ?ident) ?ns] 34 | [((complement contains?) ?system-ns ?ns)]] 35 | db system-ns)) 36 | 37 | (defn eid->map [db eid] 38 | (->> eid 39 | (d/entity db) 40 | d/touch 41 | (into {:db/id eid}))) 42 | 43 | (defn infer-schema [db] 44 | (let [e->m (partial eid->map db)] 45 | (->> (get-user-schema db) 46 | (map first) 47 | sort 48 | (map e->m)))) 49 | 50 | (defn tidy-schema [schema] 51 | (->> schema 52 | (sort-by :db/id) 53 | (map (fn [m] (dissoc m :db/id))) 54 | (into []))) 55 | 56 | (defn looks-like-enum? [m] 57 | (and (map? m) 58 | (= #{:db/ident} (->> m keys (into #{}))) 59 | (keyword? (:db/ident m)) 60 | (re-seq #"[-\w]+\.[-\w]+/[-\w]+" (str (:db/ident m))))) 61 | 62 | (defn group-as-entities 63 | [user-schema] 64 | (group-by (comp namespace :db/ident) user-schema)) 65 | 66 | (defn kw-enum->entity-name [k] 67 | {:pre [(keyword? k)]} 68 | (let [s (namespace k)] 69 | (if-let [m (re-matches #"^([-\w]+)\..+$" s)] 70 | (second m) 71 | nil))) 72 | 73 | (defn group-enums-as-entities 74 | [enums] 75 | (->> enums 76 | (map :db/ident) 77 | (group-by kw-enum->entity-name))) 78 | 79 | (defn summarize-schema [db] 80 | (let [s (->> db infer-schema tidy-schema) 81 | {e true, t false} (group-by (comp boolean looks-like-enum?) s)] 82 | {:tables (group-as-entities t) 83 | :enums (group-enums-as-entities e)})) 84 | 85 | (defn infer-schema-of-entity [db e] 86 | (let [attrs (keys e) 87 | summary (summarize-schema db) 88 | t (->> summary :tables vals flatten) 89 | m (group-by :db/ident t)] 90 | (->> attrs 91 | (mapcat (fn [attr] 92 | (get m attr)))))) 93 | 94 | (comment 95 | 96 | (use 'clojure.repl) 97 | (require '[sql-datomic.repl :as r]) 98 | 99 | (->> r/db infer-schema tidy-schema pp/pprint) 100 | (def s (->> r/db infer-schema tidy-schema)) 101 | (def enums (filter looks-like-enum? s)) 102 | (def gu (group-as-entities s)) 103 | (keys gu) 104 | (pp/pprint (get gu "product")) 105 | (pp/pprint (get gu "orderline")) 106 | (pp/pprint (get gu "order")) 107 | (def ge (group-enums-as-entities enums)) 108 | (pp/pprint ge) 109 | 110 | ) 111 | -------------------------------------------------------------------------------- /src/sql_datomic/select_command.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.select-command 2 | (:require [sql-datomic.datomic :as dat] 3 | [sql-datomic.util :as util :refer [squawk]] 4 | [datomic.api :as d] 5 | [clojure.pprint :as pp])) 6 | 7 | (defn -display-raw-entities [entities] 8 | (squawk "Entities") 9 | (binding [*out* *err*] 10 | (when-not (seq entities) 11 | (println "None")) 12 | (doseq [entity entities] 13 | (pp/pprint entity) 14 | (flush)))) 15 | 16 | (defn decorate-with-db-id [entity] 17 | ;; Note: Has side-effect of turning entity into a map. 18 | (assoc (into {} entity) :db/id (:db/id entity))) 19 | 20 | (defn enhance-entities [entities fields] 21 | (let [ms (map decorate-with-db-id entities) 22 | fattrs (dat/fields-ir->attrs fields) 23 | eattrs (dat/gather-attrs-from-entities ms) 24 | attrs (dat/resolve-attrs fattrs eattrs) 25 | consts (remove keyword? attrs)] 26 | {:entities (dat/supplement-with-consts consts ms) 27 | :attrs attrs})) 28 | 29 | (defn -run-db-id-select 30 | [db {:keys [where fields] :as ir} {:keys [debug]}] 31 | (let [ids (dat/db-id-clause-ir->eids ir) 32 | es (util/get-entities-by-eids db ids) 33 | raw-entities (dat/keep-genuine-entities es) 34 | {:keys [entities attrs]} (enhance-entities raw-entities fields)] 35 | (when debug (-display-raw-entities raw-entities)) 36 | {:ids ids 37 | :raw-entities raw-entities 38 | :entities entities 39 | :attrs attrs})) 40 | 41 | (defn -run-normal-select 42 | [db {:keys [where fields]} {:keys [debug]}] 43 | (let [query (dat/where->datomic-q db where)] 44 | (when debug 45 | (squawk "Datomic Rules" dat/rules) 46 | (squawk "Datomic Query" query)) 47 | (let [results (d/q query db dat/rules)] 48 | (when debug (squawk "Raw Results" results)) 49 | (let [raw-entities (dat/hydrate-results db results) 50 | {:keys [entities attrs]} (enhance-entities raw-entities fields)] 51 | (when debug (-display-raw-entities raw-entities)) 52 | {:query query 53 | ;; :ids ids 54 | :raw-entities raw-entities 55 | :entities entities 56 | :attrs attrs})))) 57 | 58 | (defn run-select 59 | ([db ir] (run-select db ir {})) 60 | ([db {:keys [where fields] :as ir} opts] 61 | {:pre [(= :select (:type ir))]} 62 | (when (seq where) 63 | (if (dat/db-id-clause? where) 64 | (-run-db-id-select db ir opts) 65 | (-run-normal-select db ir opts))))) 66 | -------------------------------------------------------------------------------- /src/sql_datomic/tabula.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.tabula 2 | (:require [clojure.pprint :as pp] 3 | [clojure.string :as str] 4 | [clojure.set :as set]) 5 | (:import [datomic.query EntityMap])) 6 | 7 | (defn entity-map? [e] 8 | (isa? (class e) EntityMap)) 9 | 10 | (defn abbreviate-entity [entity] 11 | (if (and (map? entity) 12 | (:db/id entity)) 13 | (select-keys entity [:db/id]) 14 | entity)) 15 | 16 | (defn abbreviate-entity-maps [entity] 17 | (->> entity 18 | (map (fn [[k v]] 19 | (let [v' (if (entity-map? v) 20 | (abbreviate-entity v) 21 | v)] 22 | [k v']))) 23 | (into {}))) 24 | 25 | (def cardinality-many? set?) 26 | 27 | (defn select-cardinality-many-attrs [entity] 28 | (->> entity 29 | (keep (fn [[k v]] 30 | (when (cardinality-many? v) 31 | k))) 32 | sort 33 | vec)) 34 | 35 | (defn abbreviate-cardinality-many-attrs [entity] 36 | (->> entity 37 | (map (fn [[k v]] 38 | (let [v' (if (cardinality-many? v) 39 | (->> v (map abbreviate-entity) (into #{})) 40 | v)] 41 | [k v']))) 42 | (into {}))) 43 | 44 | (defn elide-cardinality-manys [entity] 45 | (->> entity 46 | (remove (fn [[_ v]] (cardinality-many? v))) 47 | (into {}))) 48 | 49 | (defn string->single-quoted-string [s] 50 | (str \' (str/escape s {\' "\\'"}) \')) 51 | 52 | (defn ->printable [v] 53 | (if (string? v) 54 | (string->single-quoted-string v) 55 | (pr-str v))) 56 | 57 | (defn entity->printable-row [entity] 58 | (->> entity 59 | (map (fn [[k v]] 60 | [(->printable k) (->printable v)])) 61 | (into {}))) 62 | 63 | (def process-entity (comp entity->printable-row 64 | abbreviate-entity-maps 65 | elide-cardinality-manys)) 66 | 67 | (defn -print-elided-cardinality-many-attrs 68 | ([rows] 69 | (when (seq rows) 70 | (let [attrs (select-cardinality-many-attrs (first rows))] 71 | (when (seq attrs) 72 | (println "Elided cardinality-many attrs: " attrs))))) 73 | ([ks rows] 74 | (let [ks' (filter keyword? ks)] 75 | (->> rows 76 | (map (fn [row] (select-keys row ks'))) 77 | -print-elided-cardinality-many-attrs)))) 78 | 79 | (defn -print-row-count [rows] 80 | (printf "(%d rows)\n" (count rows))) 81 | 82 | (defn -doctor-lead-row [rows] 83 | (if-not (seq rows) 84 | rows 85 | ;; pp/print-table will use the keys of the first row as a 86 | ;; template for the columns to display. 87 | (let [all-keys (->> rows 88 | (mapcat keys) 89 | (into #{})) 90 | lead-row (first rows) 91 | tail-rows (rest rows) 92 | missing-keys (set/difference all-keys 93 | (->> lead-row keys (into #{}))) 94 | missing-map (->> missing-keys 95 | (map (fn [k] [k nil])) 96 | (into {})) 97 | doc-row (merge missing-map lead-row)] 98 | (into [doc-row] tail-rows)))) 99 | 100 | (defn -print-simple-table [{:keys [ks rows print-fn]}] 101 | (->> rows 102 | (map process-entity) 103 | (into []) 104 | -doctor-lead-row 105 | print-fn) 106 | (-print-row-count rows) 107 | (if (seq ks) 108 | (-print-elided-cardinality-many-attrs ks rows) 109 | (-print-elided-cardinality-many-attrs rows)) 110 | (println)) 111 | 112 | (defn print-simple-table 113 | ([ks rows] 114 | (if (seq ks) 115 | (-print-simple-table 116 | {:ks ks 117 | :rows rows 118 | :print-fn (partial pp/print-table (map ->printable ks))}) 119 | (print-simple-table rows))) 120 | ([rows] 121 | (-print-simple-table {:rows rows 122 | :print-fn pp/print-table}))) 123 | 124 | (defn -print-expanded-table [{:keys [ks rows]}] 125 | (when (seq rows) 126 | (let [ks' (if (seq ks) 127 | ks 128 | (->> rows (mapcat keys) (into #{}) sort)) 129 | pks (map ->printable ks') 130 | k-max-len (->> pks 131 | (map (comp count str)) 132 | (sort >) 133 | first) 134 | row-fmt (str "%-" k-max-len "s | %s\n") 135 | rows' (->> rows 136 | (map (fn [row] (select-keys row ks'))) 137 | (map (comp entity->printable-row 138 | abbreviate-entity-maps 139 | abbreviate-cardinality-many-attrs)) 140 | (map-indexed vector))] 141 | (doseq [[i row] rows'] 142 | (printf "-[ RECORD %d ]-%s\n" 143 | (inc i) 144 | (apply str (repeat 40 \-))) 145 | (let [xs (->> 146 | ;; Need ->printable keys for lookup 147 | ;; due to entity->printable-row applying ->printable 148 | ;; to row keys. 149 | pks 150 | (map (fn [k] [k (get row k "")])))] 151 | (doseq [[k v] xs] 152 | (printf row-fmt k v)))))) 153 | (-print-row-count rows) 154 | (println)) 155 | 156 | (defn print-expanded-table 157 | ([ks rows] 158 | (-print-expanded-table {:ks ks :rows rows})) 159 | ([rows] 160 | (-print-expanded-table {:rows rows}))) 161 | -------------------------------------------------------------------------------- /src/sql_datomic/types.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.types 2 | "Module for reader support for custom types 3 | 4 | Adds support for reader tagged literals: 5 | - #uri \"some://really.neato/uri/string\" 6 | - #bytes base64-str 7 | " 8 | (:import [java.nio.charset StandardCharsets] 9 | [java.util Base64] 10 | [java.net URI] 11 | [java.io Writer])) 12 | 13 | ;; Note to maintainers: 14 | ;; If you change tagged literal code here, you very likely 15 | ;; will need to modify src/data_readers.clj also (and vice versa). 16 | 17 | ;; URI support 18 | 19 | (defn ->uri [s] (URI. s)) 20 | 21 | (defmethod print-dup URI [uri ^Writer writer] 22 | (.write writer (str "#uri \"" (.toString uri) "\""))) 23 | 24 | (defmethod print-method URI [uri ^Writer writer] 25 | (print-dup uri writer)) 26 | 27 | 28 | ;; Byte Array support 29 | 30 | (defn string->bytes [^String s] 31 | (.getBytes s StandardCharsets/ISO_8859_1)) 32 | 33 | (defn bytes->string [#^bytes b] 34 | (String. b StandardCharsets/ISO_8859_1)) 35 | 36 | (defn bytes->base64-str [#^bytes b] 37 | (.encodeToString (Base64/getEncoder) b)) 38 | 39 | (defn base64-str->bytes [^String b64str] 40 | (.decode (Base64/getDecoder) b64str)) 41 | 42 | (defonce bytes-class (Class/forName "[B")) 43 | 44 | (defmethod print-dup bytes-class [#^bytes b, ^Writer writer] 45 | (.write writer (str "#bytes \"" (bytes->base64-str b) "\""))) 46 | 47 | (defmethod print-method bytes-class [#^bytes b, ^Writer writer] 48 | (print-dup b writer)) 49 | 50 | ;; http://docs.datomic.com/schema.html#bytes-limitations 51 | ;; 52 | ;; The bytes :db.type/bytes type maps directly to Java byte arrays, 53 | ;; which do not have value semantics (semantically equal byte arrays 54 | ;; do not compare or hash as equal). 55 | ;; 56 | #_(defn bytes= [& bs] 57 | (apply = (map seq bs))) 58 | 59 | 60 | ;; Float 61 | 62 | (defn ->float [v] (float v)) 63 | 64 | (defmethod print-dup Float [f ^Writer writer] 65 | (.write writer (str "#float " (.toString f) ""))) 66 | 67 | (defmethod print-method Float [f ^Writer writer] 68 | (print-dup f writer)) 69 | 70 | 71 | 72 | ;; Sometimes, clojure.lang.BigInt (e.g., 23N) will come back 73 | ;; from Datomic as java.math.BigInteger. Make them look similar. 74 | 75 | (defmethod print-dup java.math.BigInteger [bi ^Writer writer] 76 | (.write writer (str bi \N))) 77 | 78 | (defmethod print-method java.math.BigInteger [bi ^Writer writer] 79 | (print-dup bi writer)) 80 | -------------------------------------------------------------------------------- /src/sql_datomic/update_command.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.update-command 2 | (:require [sql-datomic.datomic :as dat] 3 | [sql-datomic.util :as util 4 | :refer [squawk -debug-display-entities]] 5 | [datomic.api :as d] 6 | [clojure.pprint :as pp])) 7 | 8 | (defn -run-harness [{:keys [conn db ir options ids]}] 9 | (let [{:keys [debug pretend silent]} options 10 | entities (->> (util/get-entities-by-eids db ids) 11 | dat/keep-genuine-entities)] 12 | (when debug (squawk "Entities Targeted for Update")) 13 | (when-not silent 14 | (println (if (seq entities) ids "None"))) 15 | (when debug (-debug-display-entities entities)) 16 | 17 | (if-not (seq entities) 18 | {:ids ids 19 | :before entities 20 | :pretend pretend} 21 | 22 | (let [base-tx (dat/update-ir->base-tx-data ir) 23 | tx-data (dat/stitch-tx-data base-tx ids)] 24 | (when debug (squawk "Transaction" tx-data)) 25 | (if pretend 26 | (do 27 | (println "Halting transaction due to pretend mode ON") 28 | {:ids ids 29 | :before entities 30 | :tx-data tx-data 31 | :pretend pretend}) 32 | 33 | (let [result @(d/transact conn tx-data)] 34 | (when-not silent 35 | (println) 36 | (println result)) 37 | (let [entities' (util/get-entities-by-eids (:db-after result) ids)] 38 | (when debug 39 | (squawk "Entities after Transaction") 40 | (-debug-display-entities entities')) 41 | {:ids ids 42 | :tx-data tx-data 43 | :before entities 44 | :after entities' 45 | :result result}))))))) 46 | 47 | (defn -run-db-id-update 48 | [conn db ir opts] 49 | (let [ids (dat/db-id-clause-ir->eids ir)] 50 | (-run-harness {:conn conn 51 | :db db 52 | :ir ir 53 | :options opts 54 | :ids ids}))) 55 | 56 | (defn -run-normal-update 57 | [conn db {:keys [where table assign-pairs] :as ir} 58 | {:keys [debug] :as opts}] 59 | (let [query (dat/where->datomic-q db where)] 60 | (when debug 61 | (squawk "Datomic Rules" dat/rules) 62 | (squawk "Datomic Query" query)) 63 | 64 | (let [results (d/q query db dat/rules)] 65 | (when debug (squawk "Raw Results" results)) 66 | (cond 67 | (or (->> results seq not) 68 | (->> results first count (= 1))) 69 | (let [ids (mapcat identity results)] 70 | (-run-harness {:conn conn 71 | :db db 72 | :ir ir 73 | :options opts 74 | :ids ids})) 75 | 76 | ;; Assertion: results is non-empty 77 | ;; Assertion: tuples in results have arity > 1 78 | (seq table) 79 | (let [i (util/infer-entity-index ir query) 80 | ids (map (fn [row] (nth row i)) results)] 81 | (when debug 82 | (binding [*out* *err*] 83 | (printf "Narrowing entity vars to index: %d\n" i) 84 | (prn [:ids ids]))) 85 | (-run-harness {:conn conn 86 | :db db 87 | :ir ir 88 | :options opts 89 | :ids ids})) 90 | 91 | ;; Assertion: no given table 92 | :else 93 | (let [table-names (->> assign-pairs 94 | util/summarize-ir-columns-by-table 95 | keys)] 96 | (if (= 1 (count table-names)) 97 | (let [t (first table-names) 98 | i (util/infer-entity-index ir query t) 99 | ids (map (fn [row] (nth row i)) results)] 100 | (when debug 101 | (binding [*out* *err*] 102 | (printf "Narrowing entity vars to index: %d\n" i) 103 | (prn [:ids ids]))) 104 | (-run-harness {:conn conn 105 | :db db 106 | :ir ir 107 | :options opts 108 | :ids ids})) 109 | ;; else 110 | (throw 111 | (IllegalArgumentException. 112 | "\n!!! Missing update target. Necessary with joins. Aborting update !!!\n")))))))) 113 | 114 | (defn run-update 115 | ([conn db ir] (run-update conn db ir {})) 116 | ([conn db {:keys [where] :as ir} opts] 117 | {:pre [(= :update (:type ir))]} 118 | (when (seq where) 119 | (if (dat/db-id-clause? where) 120 | (-run-db-id-update conn db ir opts) 121 | (-run-normal-update conn db ir opts))))) 122 | -------------------------------------------------------------------------------- /src/sql_datomic/util.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.util 2 | (:require [clojure.string :as str] 3 | [clojure.pprint :as pp] 4 | [datomic.api :as d])) 5 | 6 | (defn vec->list [v] 7 | (->> v rseq (into '()))) 8 | 9 | (defn get-entities-by-eids [db eids] 10 | (for [eid eids] 11 | (->> eid 12 | (d/entity db) 13 | d/touch))) 14 | 15 | (defn squawk 16 | ([title] (squawk title nil)) 17 | ([title data] 18 | (let [s (str title ":") 19 | sep (str/join (repeat (count s) \=))] 20 | (binding [*out* *err*] 21 | (println (str "\n" s "\n" sep)) 22 | (when data 23 | (pp/pprint data)) 24 | (flush))))) 25 | 26 | (defn -debug-display-entities [entities] 27 | (doseq [entity entities] 28 | (binding [*out* *err*] 29 | (pp/pprint entity) 30 | (flush)))) 31 | 32 | (defn -debug-display-entities-by-ids [db ids] 33 | (-debug-display-entities (get-entities-by-eids db ids))) 34 | 35 | (defn ir-table-column? [x] 36 | (and (map? x) (string? (:table x)) (string? (:column x)))) 37 | 38 | (defn scrape-ir-table-columns [ir] 39 | (->> (tree-seq coll? seq ir) 40 | (filter ir-table-column?) 41 | (into #{}))) 42 | 43 | (defn summarize-ir-columns-by-table [ir] 44 | (group-by :table (scrape-ir-table-columns ir))) 45 | 46 | (defn entity-var? [x] 47 | (and (symbol? x) 48 | (re-seq #"^\?e" (name x)))) 49 | 50 | (defn value-var? [x] 51 | (and (symbol? x) 52 | (re-seq #"^\?v" (name x)))) 53 | 54 | (defn base-clause? [x] 55 | (and (vector? x) 56 | (>= (count x) 3) 57 | (keyword? (nth x 1)) 58 | (entity-var? (nth x 0)) 59 | (value-var? (nth x 2)))) 60 | 61 | (defn scrape-dat-query->base-clauses [dat-query] 62 | (->> dat-query 63 | (drop-while #(not= % :where)) 64 | rest 65 | (filter base-clause?))) 66 | 67 | (defn scrape-dat-query->entity-vars [dat-query] 68 | (->> dat-query 69 | (drop-while #(not= % :find)) 70 | rest 71 | (take-while #(not (#{:where :in :with} %))) 72 | (filter entity-var?))) 73 | 74 | (defn summarize-base-clauses-by-entity-vars [dat-query] 75 | (group-by first (scrape-dat-query->base-clauses dat-query))) 76 | 77 | 78 | ;; Updates/Deletes should be carried out with respect to 79 | ;; one and only one "table". 80 | (defn infer-entity-index 81 | ([{:keys [table] :as ir} dat-query] 82 | (infer-entity-index ir dat-query table)) 83 | 84 | ([ir dat-query table] 85 | {:pre [(seq table)]} 86 | 87 | (let [entity-vars (scrape-dat-query->entity-vars dat-query) 88 | entity-vars->index (->> entity-vars 89 | (map-indexed (comp vec rseq vector)) 90 | (into {})) 91 | base-clauses (scrape-dat-query->base-clauses dat-query)] 92 | (let [vars (->> entity-vars 93 | (filter (fn [var] 94 | (when-let [meta-tables (-> var meta :ir)] 95 | (get meta-tables table)))))] 96 | (case (count vars) 97 | 0 (throw (ex-info "No entity vars associated with given table" 98 | {:given-table table 99 | :entity-vars entity-vars 100 | :associations 101 | (zipmap entity-vars 102 | (map #(-> % meta :ir) entity-vars))})) 103 | 104 | 1 (->> vars first entity-vars->index) 105 | 106 | ;; else 107 | ;; Not sure how we would end up here. 108 | (throw (ex-info "Ambiguous association of entity vars with table" 109 | {:given-table table 110 | :entity-vars vars 111 | :associations 112 | (zipmap vars 113 | (map #(-> % meta :ir) vars))}))))))) 114 | -------------------------------------------------------------------------------- /test/sql_datomic/integration_test.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.integration-test 2 | (:require [clojure.test :refer :all] 3 | [sql-datomic.datomic :as dat] 4 | sql-datomic.types ;; necessary for reader literals 5 | [sql-datomic.parser :as par] 6 | [sql-datomic.select-command :as sel] 7 | [sql-datomic.insert-command :as ins] 8 | [sql-datomic.update-command :as upd] 9 | [sql-datomic.delete-command :as del] 10 | [sql-datomic.retract-command :as rtr] 11 | [datomic.api :as d] 12 | [clojure.string :as str] 13 | [clojure.set :as set])) 14 | 15 | 16 | ;;;; SETUP and HELPER FUNCTIONS ;;;; 17 | 18 | (def ^:dynamic *conn* :not-a-connection) 19 | 20 | (def ^:dynamic *db* :not-a-db) 21 | 22 | (defn current-db [] (d/db *conn*)) 23 | 24 | (defn entity->map 25 | ([eid] (entity->map *db* eid)) 26 | ([db eid] 27 | (if-let [e (d/entity db eid)] 28 | (->> e d/touch (into {:db/id (:db/id e)})) 29 | {}))) 30 | 31 | (defn product-map->comparable [m] 32 | ;; cannot compare byte arrays and :db/id changes per db refresh 33 | (dissoc m :db/id :product/blob)) 34 | 35 | (defn -select-keys [ks m] 36 | (select-keys m ks)) 37 | 38 | (defn -select-resultset [{:keys [entities attrs]}] 39 | (into #{} (map (partial -select-keys attrs) entities))) 40 | 41 | (defn count-entities 42 | ([db primary-attr] 43 | (let [n (d/q '[:find (count ?e) . 44 | :in $ ?attr 45 | :where [?e ?attr]] 46 | db 47 | primary-attr)] 48 | (if (nil? n) 0 n))) 49 | ([db attr v] 50 | (let [n (d/q '[:find (count ?e) . 51 | :in $ ?attr ?v 52 | :where [?e ?attr ?v]] 53 | db 54 | attr v)] 55 | (if (nil? n) 0 n)))) 56 | 57 | (defn db-fixture [f] 58 | (let [sys (.start (dat/system {}))] 59 | #_(do (println "=== db set-up") (flush)) 60 | (binding [*conn* (->> sys :datomic :connection)] 61 | (binding [*db* (d/db *conn*)] 62 | (f))) 63 | (.stop sys) 64 | #_(do (println "=== db tear-down") (flush)))) 65 | 66 | (use-fixtures :each db-fixture) 67 | 68 | 69 | ;;;; SELECT statements ;;;; 70 | 71 | (deftest product-entity-present 72 | (is (= (product-map->comparable (entity->map *db* [:product/prod-id 8293])) 73 | {:product/url #uri "http://example.com/products/8293" 74 | :product/prod-id 8293 75 | :product/uuid #uuid "57607472-0bd8-4ed3-98d3-586e9e9c9683" 76 | :product/common-prod-id 4804 77 | :product/man-hours 100N 78 | :product/price 13.99M 79 | :product/category :product.category/family 80 | :product/tag :alabama-exorcist-family 81 | :product/actor "NICK MINELLI" 82 | :product/rating #float 2.6 83 | :product/special false 84 | :product/title "ALABAMA EXORCIST"}))) 85 | 86 | (defn stmt->ir [stmt] 87 | (->> stmt par/parser par/transform)) 88 | 89 | (deftest select-product-by-prod-id 90 | (let [ir (stmt->ir "select where product.prod-id = 9990")] 91 | (is (= (->> (sel/run-select *db* ir) 92 | :entities 93 | (map product-map->comparable)) 94 | [{:product/url #uri "http://example.com/products/9990" 95 | :product/prod-id 9990 96 | :product/uuid #uuid "57607426-cdd4-49fa-aecb-0a2572976db9" 97 | :product/common-prod-id 6584 98 | :product/man-hours 60100200300N 99 | :product/price 25.99M 100 | :product/category :product.category/music 101 | :product/tag :aladdin-world-music 102 | :product/actor "HUMPHREY DENCH" 103 | :product/rating #float 2.0 104 | :product/special false 105 | :product/title "ALADDIN WORLD"}])))) 106 | 107 | (deftest select-all-products-by-prod-id 108 | (let [ir (stmt->ir "select where product.prod-id > 0") 109 | prod-ids #{1298 1567 110 | 2290 2926 111 | 4402 4936 112 | 5130 113 | 6127 6376 6879 114 | 8293 115 | 9990}] 116 | (is (= (->> (sel/run-select *db* ir) 117 | :entities 118 | (map :product/prod-id) 119 | (into #{})) 120 | prod-ids)))) 121 | 122 | (deftest select-prod-id-6k-products 123 | (let [ir (stmt->ir 124 | "select where product.prod-id between 6000 and 6999") 125 | prod-ids #{6127 6376 6879}] 126 | (is (= (->> (sel/run-select *db* ir) 127 | :entities 128 | (map :product/prod-id) 129 | (into #{})) 130 | prod-ids)))) 131 | 132 | (deftest select-no-products-by-prod-id 133 | (let [ir (stmt->ir 134 | "select where product.prod-id >= 10000") 135 | prod-ids #{}] 136 | (is (= (->> (sel/run-select *db* ir) 137 | :entities 138 | (map :product/prod-id) 139 | (into #{})) 140 | prod-ids)))) 141 | 142 | (deftest select-order-between-dates 143 | (let [ir (stmt->ir 144 | "select where 145 | order.orderdate between #inst \"2004-01-01\" 146 | and #inst \"2004-01-05\" ")] 147 | (is (= (->> (sel/run-select *db* ir) 148 | :entities 149 | (map (partial -select-keys 150 | [:order/orderid :order/orderdate])) 151 | (into #{})) 152 | #{{:order/orderid 2 153 | :order/orderdate #inst "2004-01-01T08:00:00.000-00:00"}})))) 154 | 155 | (deftest select-cols-of-product-by-prod-id 156 | (let [ir (stmt->ir 157 | "select product.prod-id, #attr :product/tag, product.title 158 | where product.prod-id = 9990")] 159 | (is (= (-select-resultset (sel/run-select *db* ir)) 160 | #{{:product/prod-id 9990 161 | :product/tag :aladdin-world-music 162 | :product/title "ALADDIN WORLD"}})))) 163 | 164 | (deftest select-join 165 | (let [ir (stmt->ir 166 | "select order.orderid, 167 | order.totalamount, 168 | order.customerid, 169 | customer.email, 170 | orderline.orderlineid, 171 | orderline.prod-id, 172 | product.uuid, 173 | product.category, 174 | orderline.quantity 175 | where order.orderid = orderline.orderid 176 | and order.customerid = customer.customerid 177 | and orderline.prod-id = product.prod-id 178 | 179 | and order.orderid in (1, 2, 3) 180 | and orderline.quantity > 2 181 | and product.category <> :product.category/new 182 | ")] 183 | (is (= (-select-resultset (sel/run-select *db* ir)) 184 | #{{:order/orderid 2 185 | :order/totalamount 59.43M 186 | :order/customerid 4858 187 | :customer/email "OVWOIYIDDL@dell.com" 188 | :orderline/orderlineid 8 189 | :orderline/prod-id 2926 190 | :product/uuid #uuid "576073e7-d671-45ba-af1a-f08a9a355b81" 191 | :product/category :product.category/music 192 | :orderline/quantity 3} 193 | {:order/orderid 2 194 | :order/totalamount 59.43M 195 | :order/customerid 4858 196 | :customer/email "OVWOIYIDDL@dell.com" 197 | :orderline/orderlineid 4 198 | :orderline/prod-id 5130 199 | :product/uuid #uuid "5760740c-4f24-4f3d-8455-f79a1cc57fa9" 200 | :product/category :product.category/action 201 | :orderline/quantity 3}})))) 202 | 203 | ;; | :product/prod-id | :product/rating | :product/price | :product/category | :product/man-hours | cat | pr | mh | ?? 204 | ;; |------------------+-----------------+----------------+-------------------------------+--------------------| 205 | ;; | 2926 | #float 1.6 | 22.99M | :product.category/music | 40100200300N | t f f t 206 | ;; | 5130 | #float 1.8 | 14.99M | :product.category/action | 50100200300N | t t f t 207 | ;; | 9990 | #float 2.0 | 25.99M | :product.category/music | 60100200300N | t f f t 208 | ;; | 1567 | #float 2.2 | 25.99M | :product.category/new | 70100200300N | f f f f 209 | ;; | 6376 | #float 2.4 | 21.99M | :product.category/drama | 80100200300N | f f f f 210 | ;; | 8293 | #float 2.6 | 13.99M | :product.category/family | 100N | t t t t 211 | ;; | 4402 | #float 2.8 | 11.99M | :product.category/children | 101N | f t t t 212 | ;; | 6879 | #float 3.0 | 12.99M | :product.category/action | 102N | t t t t 213 | ;; | 2290 | #float 3.2 | 15.99M | :product.category/documentary | 103N | f t t t 214 | (deftest select-and-or-not-oh-my 215 | (let [ir (stmt->ir 216 | "select product.prod-id, 217 | product.category, 218 | product.price, 219 | product.man-hours, 220 | product.rating 221 | where ( product.category in ( 222 | :product.category/music, 223 | :product.category/action, 224 | :product.category/family, 225 | :product.category/horror) 226 | or ( (not (product.price between 20.0M and 30.0M)) 227 | and (not (product.man-hours > 1000N)))) 228 | and product.rating > 1.5f")] 229 | (is (= (-select-resultset (sel/run-select *db* ir)) 230 | #{{:product/prod-id 2926 231 | :product/rating #float 1.6 232 | :product/price 22.99M 233 | :product/category :product.category/music 234 | :product/man-hours 40100200300N} 235 | {:product/prod-id 5130 236 | :product/rating #float 1.8 237 | :product/price 14.99M 238 | :product/category :product.category/action 239 | :product/man-hours 50100200300N} 240 | {:product/prod-id 9990 241 | :product/rating #float 2.0 242 | :product/price 25.99M 243 | :product/category :product.category/music 244 | :product/man-hours 60100200300N} 245 | {:product/prod-id 8293 246 | :product/rating #float 2.6 247 | :product/price 13.99M 248 | :product/category :product.category/family 249 | :product/man-hours 100N} 250 | {:product/prod-id 4402 251 | :product/rating #float 2.8 252 | :product/price 11.99M 253 | :product/category :product.category/children 254 | :product/man-hours 101N} 255 | {:product/prod-id 6879 256 | :product/rating #float 3.0 257 | :product/price 12.99M 258 | :product/category :product.category/action 259 | :product/man-hours 102N} 260 | {:product/prod-id 2290 261 | :product/rating #float 3.2 262 | :product/price 15.99M 263 | :product/category :product.category/documentary 264 | :product/man-hours 103N}})))) 265 | 266 | (deftest select-by-db-id 267 | (let [db-ids (d/q '[:find [?e ...] 268 | :where 269 | [?e :product/prod-id ?pid] 270 | [(> ?pid 6500)]] 271 | *db*) 272 | stmt (str "select product.prod-id where #attr :db/id " 273 | (str/join " " db-ids)) 274 | ir (stmt->ir stmt)] 275 | (is (= (-select-resultset (sel/run-select *db* ir)) 276 | #{{:product/prod-id 9990} 277 | {:product/prod-id 8293} 278 | {:product/prod-id 6879}})))) 279 | 280 | 281 | ;;;; INSERT statements ;;;; 282 | 283 | (deftest insert-traditional-form 284 | (let [db *db* 285 | cnt (count-entities db :customer/customerid) 286 | stmt "insert into customer ( 287 | customerid, 288 | firstname, lastname, email, address-1, 289 | city, state, zip, country 290 | ) 291 | values ( 292 | 12345, 293 | 'Foo', 'Bar', 'foobar@example.org', '123 Some Place', 294 | 'Thousand Oaks', 'CA', '91362', 'USA' 295 | )" 296 | ir (stmt->ir stmt) 297 | _got (ins/run-insert *conn* ir {:silent true}) 298 | db' (d/db *conn*) 299 | e->m' (partial entity->map db') 300 | cnt' (count-entities db' :customer/customerid) 301 | ids (d/q '[:find [?e ...] 302 | :where [?e :customer/email "foobar@example.org"]] 303 | db') 304 | ents (->> ids 305 | (map e->m') 306 | (map (fn [m] (dissoc m :db/id))) 307 | (into #{}))] 308 | (is (= (inc cnt) cnt')) 309 | (is (= ents 310 | #{{:customer/customerid 12345 311 | :customer/firstname "Foo" 312 | :customer/lastname "Bar" 313 | :customer/email "foobar@example.org" 314 | :customer/address-1 "123 Some Place" 315 | :customer/city "Thousand Oaks" 316 | :customer/state "CA" 317 | :customer/zip "91362" 318 | :customer/country "USA"}})))) 319 | 320 | (deftest insert-short-form 321 | (let [db *db* 322 | cnt (count-entities db :product/prod-id) 323 | stmt "insert into 324 | #attr :product/prod-id = 9999, 325 | #attr :product/actor = 'Naomi Watts', 326 | #attr :product/title = 'The Ring', 327 | product.category = :product.category/horror, 328 | product.rating = 4.5f, 329 | product.man-hours = 9001N, 330 | product.price = 21.99M" 331 | ir (stmt->ir stmt) 332 | _got (ins/run-insert *conn* ir {:silent true}) 333 | db' (d/db *conn*) 334 | e->m' (partial entity->map db') 335 | cnt' (count-entities db' :product/prod-id) 336 | ids (d/q '[:find [?e ...] 337 | :where [?e :product/prod-id 9999]] 338 | db') 339 | ents (->> ids 340 | (map e->m') 341 | (map (fn [m] (dissoc m :db/id))) 342 | (into #{}))] 343 | (is (= (inc cnt) cnt')) 344 | (is (= ents 345 | #{{:product/prod-id 9999 346 | :product/actor "Naomi Watts" 347 | :product/title "The Ring" 348 | :product/category :product.category/horror 349 | :product/rating #float 4.5 350 | :product/man-hours 9001N 351 | :product/price 21.99M}})))) 352 | 353 | 354 | ;;;; UPDATE statements ;;;; 355 | 356 | (deftest update-customer-where-customerid 357 | (let [stmt "update customer 358 | set customer.city = 'Springfield' 359 | , customer.state = 'VA' 360 | , customer.zip = '22150' 361 | where customer.customerid = 4858" 362 | db *db* 363 | id (d/q '[:find ?e . :where [?e :customer/customerid 4858]] db) 364 | ent (entity->map db id) 365 | cnt (count-entities db :customer/customerid) 366 | ir (stmt->ir stmt) 367 | _got (upd/run-update *conn* db ir {:silent true}) 368 | db' (d/db *conn*) 369 | ent' (entity->map db' id)] 370 | (is (= ent' 371 | (assoc ent 372 | :customer/city "Springfield" 373 | :customer/state "VA" 374 | :customer/zip "22150"))) 375 | (is (= (count-entities db' :customer/customerid) cnt) 376 | "number of customers remains unchanged"))) 377 | 378 | (deftest update-products-where-db-id 379 | (let [db *db* 380 | ids (d/q '[:find [?e ...] 381 | :where 382 | [?e :product/prod-id ?pid] 383 | [(> ?pid 6000)]] 384 | db) 385 | e->m (partial entity->map db) 386 | ents (->> ids (map e->m) (into #{})) 387 | cnt (count-entities db :product/prod-id) 388 | stmt "update product 389 | set product.tag = :all-the-things 390 | , product.special = true 391 | , product.price = 1.50M 392 | where db.id " 393 | stmt' (str stmt (str/join " " ids)) 394 | ir (stmt->ir stmt') 395 | _got (upd/run-update *conn* db ir {:silent true}) 396 | db' (d/db *conn*) 397 | e->m' (partial entity->map db') 398 | ents' (->> ids (map e->m') (into #{}))] 399 | (is (= ents' 400 | (->> ents 401 | (map (fn [ent] 402 | (assoc ent 403 | :product/tag :all-the-things 404 | :product/special true 405 | :product/price 1.50M))) 406 | (into #{})))) 407 | (is (= (count-entities db' :product/prod-id) cnt) 408 | "the number of products has not changed") 409 | ;; assumption: all products originally have special set to false 410 | (when (zero? (count-entities db :product/special true)) 411 | (is (= (count ents') 412 | (count-entities db' :product/special true)))))) 413 | 414 | (deftest update-customer-order-join 415 | (let [stmt "update customer 416 | set #attr :customer/age = 45 417 | , customer.income = 70000M 418 | where customer.customerid = order.customerid 419 | and order.orderid = 5" 420 | db *db* 421 | e->m (partial entity->map db) 422 | the-order (e->m [:order/orderid 5]) 423 | the-cust (:order/customer the-order) 424 | order-cnt (count-entities db :order/orderid) 425 | cust-cnt (count-entities db :customer/customerid) 426 | ir (stmt->ir stmt) 427 | _got (upd/run-update *conn* db ir {:silent true}) 428 | db' (d/db *conn*) 429 | e->m' (partial entity->map db')] 430 | (is (= (e->m' [:order/orderid 5]) 431 | the-order) 432 | "the order was unaffected") 433 | (is (= (e->m' (:db/id the-cust)) 434 | (assoc (e->m (:db/id the-cust)) 435 | :customer/age 45 436 | :customer/income 70000M))) 437 | (is (= (count-entities db' :order/orderid) order-cnt) 438 | "the number of orders has not changed") 439 | (is (= (count-entities db' :customer/customerid) cust-cnt) 440 | "the number of customers has not changed"))) 441 | 442 | (deftest update-product-short-form 443 | (let [stmt "update #attr :product/rating = 3.5f 444 | where #attr :product/prod-id > 6000" 445 | db *db* 446 | e->m (partial entity->map db) 447 | ids (d/q '[:find [?e ...] 448 | :where 449 | [?e :product/prod-id ?pid] 450 | [(> ?pid 6000)]] 451 | db) 452 | products (->> ids (map e->m) (into #{})) 453 | cnt (count-entities db :product/prod-id) 454 | ir (stmt->ir stmt) 455 | _got (upd/run-update *conn* db ir {:silent true}) 456 | db' (d/db *conn*) 457 | e->m' (partial entity->map db') 458 | products' (->> ids (map e->m') (into #{}))] 459 | (is (= products' 460 | (->> products 461 | (map (fn [p] (assoc p :product/rating #float 3.5))) 462 | (into #{})))) 463 | (is (= (count-entities db' :product/prod-id) cnt) 464 | "the number of products has not changed"))) 465 | 466 | (deftest update-product-short-form-by-db-id 467 | (let [stmt "update #attr :product/rating = 3.5f 468 | where #attr :db/id in (" 469 | db *db* 470 | e->m (partial entity->map db) 471 | ids (d/q '[:find [?e ...] 472 | :where 473 | [?e :product/prod-id ?pid] 474 | [(< ?pid 6000)]] 475 | db) 476 | stmt' (str stmt (str/join ", " ids) ")") 477 | products (->> ids (map e->m) (into #{})) 478 | cnt (count-entities db :product/prod-id) 479 | ir (stmt->ir stmt') 480 | _got (upd/run-update *conn* db ir {:silent true}) 481 | db' (d/db *conn*) 482 | e->m' (partial entity->map db') 483 | products' (->> ids (map e->m') (into #{}))] 484 | (is (= products' 485 | (->> products 486 | (map (fn [p] (assoc p :product/rating #float 3.5))) 487 | (into #{})))) 488 | (is (= (count-entities db' :product/prod-id) cnt) 489 | "the number of products has not changed"))) 490 | 491 | (deftest update-customer-order-join-short-form 492 | (let [stmt "update #attr :customer/age = 45 493 | , customer.income = 70000M 494 | where customer.customerid = order.customerid 495 | and order.orderid = 5" 496 | ;; this works so long as all attrs in the assignment pair list 497 | ;; refer to one (and only one) namespace/table. 498 | db *db* 499 | e->m (partial entity->map db) 500 | the-order (e->m [:order/orderid 5]) 501 | the-cust (:order/customer the-order) 502 | order-cnt (count-entities db :order/orderid) 503 | cust-cnt (count-entities db :customer/customerid) 504 | ir (stmt->ir stmt) 505 | _got (upd/run-update *conn* db ir {:silent true}) 506 | db' (d/db *conn*) 507 | e->m' (partial entity->map db')] 508 | (is (= (e->m' [:order/orderid 5]) 509 | the-order) 510 | "the order was unaffected") 511 | (is (= (e->m' (:db/id the-cust)) 512 | (assoc (e->m (:db/id the-cust)) 513 | :customer/age 45 514 | :customer/income 70000M))) 515 | (is (= (count-entities db' :order/orderid) order-cnt) 516 | "the number of orders has not changed") 517 | (is (= (count-entities db' :customer/customerid) cust-cnt) 518 | "the number of customers has not changed"))) 519 | 520 | 521 | ;;;; DELETE statements ;;;; 522 | 523 | (deftest delete-targeting-no-rows-has-no-effect 524 | (let [actor "homer simpson" 525 | stmt (format "delete from product where product.actor = '%s'" 526 | actor) 527 | db *db* 528 | product-cnt (count-entities db :product/prod-id) 529 | ;; should be empty 530 | ids (d/q '[:find [?e ...] 531 | :in $ ?doh! 532 | :where [?e :product/actor ?doh!]] 533 | db actor) 534 | ir (stmt->ir stmt) 535 | _got (del/run-delete *conn* db ir {:silent true}) 536 | db' (d/db *conn*)] 537 | (is (empty? ids) "assumption: where clause matched no rows") 538 | (is (= (count-entities db' :product/prod-id) 539 | product-cnt) 540 | "the number of products has not changed") 541 | (is (= db db') "the db itself has not changed"))) 542 | 543 | (deftest delete-product-by-prod-id 544 | (let [prod-id 9990 545 | stmt (format "delete from product where product.prod-id = %d" 546 | prod-id) 547 | db *db* 548 | e->m (partial entity->map db) 549 | query '[:find ?e 550 | :in $ ?pid 551 | :where [?e :product/prod-id ?pid]] 552 | product (e->m [:product/prod-id prod-id]) 553 | product-cnt (count-entities db :product/prod-id) 554 | ir (stmt->ir stmt) 555 | _got (del/run-delete *conn* db ir {:silent true}) 556 | db' (d/db *conn*) 557 | e->m' (partial entity->map db')] 558 | (is (not-empty (d/q query db prod-id)) 559 | "assumption: product targeted for deletion was present initially") 560 | (is (= (count-entities db' :product/prod-id) 561 | (dec product-cnt)) 562 | "the number of products has decreased by one") 563 | (is (empty? (d/q query db' prod-id)) 564 | "product targeted for deletion was retracted"))) 565 | 566 | (deftest delete-all-products-by-prod-id 567 | ;; chose products here because they have no component attrs, 568 | ;; i.e., they will not trigger a cascading delete. 569 | (let [stmt "delete from product where product.prod-id > 0" 570 | db *db* 571 | query '[:find [?e ...] 572 | :where [?e :product/prod-id]] 573 | ids (d/q query db) 574 | ir (stmt->ir stmt) 575 | _got (del/run-delete *conn* db ir {:silent true}) 576 | db' (d/db *conn*)] 577 | (is (pos? (count-entities db :product/prod-id)) 578 | "assumption: initially, db had products present") 579 | (is (zero? (count-entities db' :product/prod-id)) 580 | "the number of products has decreased to zero") 581 | (is (empty? (d/q query db')) 582 | "no products are present in db"))) 583 | 584 | (deftest delete-products-by-db-id 585 | (let [stmt "delete from product where db.id " 586 | db *db* 587 | e->m (partial entity->map db) 588 | product-cnt (count-entities db :product/prod-id) 589 | query '[:find [?e ...] 590 | :where 591 | [?e :product/prod-id ?pid] 592 | [(<= ?pid 7000)] 593 | [(<= 4000 ?pid)]] 594 | ids (d/q query db) 595 | stmt' (str stmt (str/join " " ids)) 596 | ir (stmt->ir stmt') 597 | _got (del/run-delete *conn* db ir {:silent true}) 598 | db' (d/db *conn*) 599 | e->m' (partial entity->map db') 600 | query' '[:find [?e ...] :where [?e :product/prod-id]] 601 | ids' (d/q query' db') 602 | products' (map e->m' ids')] 603 | (is (= (count-entities db' :product/prod-id) 604 | (- product-cnt (count ids))) 605 | "the number of products has decreased appropriately") 606 | (is (empty? (set/intersection 607 | (into #{} ids') 608 | (into #{} ids))) 609 | "targeted :db/ids no longer present in db") 610 | (is (not-any? (fn [{:keys [product/prod-id]}] 611 | (<= 4000 prod-id 7000)) 612 | products')))) 613 | 614 | (deftest delete-order-cascading-orderlines-by-orderid 615 | (let [stmt "delete from order where order.orderid = 5" 616 | db *db* 617 | e->m (partial entity->map db) 618 | ;; an order has many orderlines (as components) 619 | ;; an orderline has a product (not component) 620 | ;; deleting an order should delete its orderlines but not the products 621 | order-cnt (count-entities db :order/orderid) 622 | orderline-cnt (count-entities db :orderline/orderlineid) 623 | product-cnt (count-entities db :product/prod-id) 624 | oid (d/q '[:find ?e . :where [?e :order/orderid 5]] db) 625 | order (e->m oid) 626 | olids (d/q '[:find [?e ...] :where [?e :orderline/orderid 5]] db) 627 | orderlines (map e->m olids) 628 | pids (d/q '[:find [?e ...] 629 | :where 630 | [?ol :orderline/orderid 5] 631 | [?ol :orderline/prod-id ?pid] 632 | [?e :product/prod-id ?pid]] db) 633 | products (map e->m pids) 634 | prod-ids (map :product/prod-id products) 635 | ir (stmt->ir stmt) 636 | _got (del/run-delete *conn* db ir {:silent true}) 637 | db' (d/db *conn*) 638 | e->m' (partial entity->map db')] 639 | (is (empty? (d/q '[:find ?e :where [?e :order/orderid 5]] db')) 640 | "targeted order is no longer present in db") 641 | (is (empty? (d/q '[:find ?e :where [?e :orderline/orderid 5]] db')) 642 | "targeted order's orderlines are no longer present in db") 643 | (is (not-empty 644 | (d/q '[:find ?e 645 | :in $ [?pid ...] 646 | :where [?e :product/prod-id ?pid]] 647 | db' prod-ids)) 648 | "targeted order's orderlines' products are still present in db") 649 | (is (= (count-entities db' :order/orderid) 650 | (dec order-cnt)) 651 | "the number of orders has decreased by one") 652 | (is (= (count-entities db' :orderline/orderlineid) 653 | (- orderline-cnt (count olids))) 654 | "the number of orderlines has decreased appropriately") 655 | (is (= (count-entities db' :product/prod-id) 656 | product-cnt) 657 | "the number of products has not changed"))) 658 | 659 | (deftest delete-product-by-prod-id-short-form 660 | (let [prod-id 1567 661 | stmt (format "delete where #attr :product/prod-id = %d" 662 | prod-id) 663 | db *db* 664 | e->m (partial entity->map db) 665 | query '[:find ?e 666 | :in $ ?pid 667 | :where [?e :product/prod-id ?pid]] 668 | product (e->m [:product/prod-id prod-id]) 669 | product-cnt (count-entities db :product/prod-id) 670 | ir (stmt->ir stmt) 671 | _got (del/run-delete *conn* db ir {:silent true}) 672 | db' (d/db *conn*) 673 | e->m' (partial entity->map db')] 674 | (is (not-empty (d/q query db prod-id)) 675 | "assumption: product targeted for deletion was present initially") 676 | (is (= (count-entities db' :product/prod-id) 677 | (dec product-cnt)) 678 | "the number of products has decreased by one") 679 | (is (empty? (d/q query db' prod-id)) 680 | "product targeted for deletion was retracted"))) 681 | 682 | (deftest delete-orderlines-join-by-orderid 683 | (let [stmt "delete from orderline 684 | where orderline.orderid = order.orderid 685 | and order.orderid = 2 686 | and orderline.quantity = 1" 687 | ;; note that we are targeting orderlines for deletion, not order 688 | db *db* 689 | e->m (partial entity->map db) 690 | order-cnt (count-entities db :order/orderid) 691 | orderline-cnt (count-entities db :orderline/orderlineid) 692 | product-cnt (count-entities db :product/prod-id) 693 | target-olids (d/q '[:find [?e ...] 694 | :where 695 | [?e :orderline/orderid 2] 696 | [?e :orderline/quantity 1]] db) 697 | target-orderline-ids (->> target-olids 698 | (map e->m) 699 | (map :orderline/orderlineid)) 700 | remain-olids (d/q '[:find [?e ...] 701 | :where 702 | [?e :orderline/orderid 2] 703 | [?e :orderline/quantity ?n] 704 | [(not= ?n 1)]] db) 705 | pids (d/q '[:find [?e ...] 706 | :where 707 | [?ol :orderline/orderid 2] 708 | [?ol :orderline/prod-id ?pid] 709 | [?e :product/prod-id ?pid]] db) 710 | prod-ids (->> pids 711 | (map e->m) 712 | (map :product/prod-id)) 713 | ir (stmt->ir stmt) 714 | _got (del/run-delete *conn* db ir {:silent true}) 715 | db' (d/db *conn*) 716 | e->m' (partial entity->map db') 717 | orderlines' (->> (d/q '[:find [?e ...] 718 | :where [?e :orderline/orderid 2]] db') 719 | (map e->m'))] 720 | (is (= (count-entities db' :order/orderid) 721 | order-cnt) 722 | "the number of orders has not changed") 723 | (is (= (count-entities db' :orderline/orderlineid) 724 | (- orderline-cnt (count target-olids))) 725 | "the number of orderlines has decreased appropriately") 726 | (is (= (count-entities db' :product/prod-id) 727 | product-cnt) 728 | "the number of products has not changed") 729 | (is (not-empty (d/q '[:find ?e :where [?e :order/orderid 2]] db')) 730 | "the associated order is still present") 731 | (is (empty? 732 | (d/q '[:find ?e 733 | :in $ [?olid ...] 734 | :where 735 | [?e :orderline/orderid 2] 736 | [?e :orderline/orderlineid ?olid]] 737 | db' target-orderline-ids)) 738 | "targeted orderlines of given order were retracted") 739 | (is (not-any? (fn [{:keys [orderline/quantity]}] 740 | (= 1 quantity)) 741 | orderlines') 742 | "none of the quantity orderlines of given order are present") 743 | (is (= (->> (d/q '[:find [?e ...] :where [?e :orderline/orderid 2]] db') 744 | (into #{})) 745 | (->> remain-olids (into #{}))) 746 | "all of the non-targeted orderlines are still present"))) 747 | 748 | 749 | ;;;; RETRACT statements ;;;; 750 | 751 | (deftest retract-product-attr-by-db-id 752 | (let [stmt-fmt "RETRACT product.uuid WHERE db.id = %d" 753 | db *db* 754 | e->m (partial entity->map db) 755 | product-cnt (count-entities db :product/prod-id) 756 | prod-id 8293 757 | id (d/q '[:find ?e . 758 | :in $ ?pid 759 | :where [?e :product/prod-id ?pid]] 760 | db prod-id) 761 | stmt (format stmt-fmt id) 762 | ir (stmt->ir stmt) 763 | _got (rtr/run-retract *conn* db ir {:silent true}) 764 | db' (d/db *conn*) 765 | e->m' (partial entity->map db') 766 | product' (e->m' id) 767 | attrs' (->> product' keys (into #{}))] 768 | (is (= (get attrs' :product/uuid :nope) :nope) 769 | "the retracted attr is no longer present on target product") 770 | (is (= (count-entities db' :product/uuid) 771 | (dec product-cnt)) 772 | "number of products with uuid attr decreased by one") 773 | (is (= (count-entities db' :product/prod-id) 774 | product-cnt) 775 | "number of products remains unchanged"))) 776 | 777 | (deftest retract-product-attrs-for-many-db-ids 778 | (let [stmt "retract #attr :product/actor, 779 | #attr :product/rating, 780 | #attr :product/url 781 | where db.id " 782 | db *db* 783 | e->m (partial entity->map db) 784 | retracted-attrs [:product/actor :product/rating :product/url] 785 | product-cnt (count-entities db :product/prod-id) 786 | ids (d/q '[:find [?e ...] 787 | :where 788 | [?e :product/prod-id ?pid] 789 | [(> ?pid 6000)]] 790 | db) 791 | remain-ids (d/q '[:find [?e ...] 792 | :where 793 | [?e :product/prod-id ?pid] 794 | [(<= ?pid 6000)]] 795 | db) 796 | remain-products (->> remain-ids (map e->m)) 797 | all-attrs (->> remain-products first keys (into #{})) 798 | kept-attrs (set/difference all-attrs (into #{} retracted-attrs)) 799 | stmt' (str stmt (str/join " " ids)) 800 | ir (stmt->ir stmt') 801 | _got (rtr/run-retract *conn* db ir {:silent true}) 802 | db' (d/db *conn*) 803 | e->m' (partial entity->map db') 804 | products' (->> ids (map e->m')) 805 | remain-products' (->> remain-ids (map e->m'))] 806 | (is (= (count-entities db' :product/prod-id) 807 | product-cnt) 808 | "number of products remains unchanged") 809 | (is (= (into #{} remain-products') 810 | (into #{} remain-products)) 811 | "the non-targeted products remain unchanged") 812 | (is (every? (fn [p] 813 | (let [attrs (->> p keys (into #{}))] 814 | (= kept-attrs attrs))) 815 | products') 816 | "the retracted attrs are no longer present on target products"))) 817 | 818 | (comment 819 | 820 | (defn pp-ent [eid] 821 | (db-fixture 822 | (fn [] 823 | (->> (entity->map *db* eid) 824 | clojure.pprint/pprint)))) 825 | (pp-ent [:product/prod-id 9990]) 826 | 827 | ) 828 | -------------------------------------------------------------------------------- /test/sql_datomic/parser_test.clj: -------------------------------------------------------------------------------- 1 | (ns sql-datomic.parser-test 2 | (:require [clojure.test :refer :all] 3 | [sql-datomic.parser :as prs] 4 | [instaparse.core :as insta] 5 | [clj-time.core :as tm] 6 | [clojure.instant :as inst])) 7 | 8 | (defmacro parsable? 9 | ([stmt] `(is (prs/good-ast? (prs/parser ~stmt)))) 10 | ([stmt msg] `(is (prs/good-ast? (prs/parser ~stmt)) ~msg))) 11 | 12 | (deftest parser-tests 13 | (testing "SELECT statements" 14 | (parsable? "SELECT a_table.name FROM a_table" 15 | "allows simple queries") 16 | (parsable? "SELECT a_table.name, a_table.age FROM a_table" 17 | "allows multiple columns in select list") 18 | (parsable? "SELECT a_table.* FROM a_table" 19 | "allows qualified star in select list") 20 | (parsable? "SELECT a_table.name FROM a_table, b_table" 21 | "allows implicit cartesian product in from list") 22 | (parsable? 23 | "SELECT a_table.foo 24 | FROM a_table 25 | " "tolerant of newlines") 26 | (parsable? 27 | "SELECT a_table.foo,\r 28 | a_table.bar,\r 29 | a_table.baz\r 30 | FROM a_table\r 31 | " "tolerant of tabs and carriage returns") 32 | (parsable? 33 | " 34 | SELECT a_table.foo FROM a_table 35 | " "tolerant of newlines at beginning and end") 36 | (parsable? "SELECT a_table.name FROM a_table WHERE a_table.name = 'foo'" 37 | "allows string literals") 38 | (parsable? 39 | "SELECT a_table.*, b_table.zebra_id 40 | FROM a_table 41 | , b_table 42 | WHERE a_table.id = b_table.a_id 43 | AND b_table.zebra_id > 9000" 44 | "supports where clause conjoined by and") 45 | (parsable? 46 | "SELECT a_table.*, b_table.zebra_id 47 | FROM a_table, b_table 48 | WHERE a_table.id = b_table.a_id 49 | ") 50 | (parsable? 51 | "SELECT a_table.*, b_table.zebra_id 52 | FROM a_table, b_table 53 | WHERE a_table.id = b_table.a_id 54 | AND b_table.zebra_id > 9000 55 | ") 56 | (parsable? 57 | "SELECT a_table.* 58 | FROM a_table 59 | WHERE a_table.foo <> 0" 60 | "supports <> as binary comparison in where clauses") 61 | (parsable? 62 | "SELECT a_table.*, b_table.zebra_id 63 | FROM a_table 64 | , b_table 65 | WHERE a_table.id = b_table.a_id 66 | AND b_table.zebra_id != 0 67 | " "supports != as binary comparison in where clauses") 68 | (parsable? 69 | "select 70 | a_table.* 71 | , b_table.zebra_id 72 | from 73 | a_table 74 | , b_table 75 | where 76 | a_table.id = b_table.a_id 77 | and b_table.zebra_id > 0 78 | " "tolerant of lowercase res words and exotic whitespace formatting") 79 | (parsable? 80 | "SELECT a_table.*, b_table.zebra_id 81 | FROM a_table, b_table 82 | WHERE a_table.id = b_table.a_id 83 | AND b_table.zebra_id > 9000") 84 | (parsable? 85 | "SELECT a_table.* 86 | FROM a_table 87 | WHERE a_table.created_on BETWEEN DATE '2007-02-01' 88 | AND DATE '2010-10-10'" 89 | "supports between with date literals") 90 | (parsable? 91 | "select foo.name 92 | from foo 93 | where foo.title = 'git-\\'r-dun maestro' 94 | and foo.hired_on <= date '2000-01-01'" 95 | "allows string literals with embedded single quote") 96 | (parsable? 97 | "select foo.id, foo.name, foo.title 98 | from foo 99 | where foo.is_active = true 100 | and foo.group_id >= 3" 101 | "supports boolean literals in where clause") 102 | (parsable? 103 | "select survey-request.email, 104 | survey-request.completion-status 105 | from survey-request 106 | where survey-request.sent-date between date '2014-04-01' 107 | and date '2014-05-01' 108 | " 109 | "supports hyphenation in table/column identifiers") 110 | (parsable? 111 | "SELECT a_table.* 112 | FROM a_table 113 | WHERE a_table.created_on BETWEEN DATETIME '2007-02-01T10:11:12' 114 | AND #inst \"2010-10-10T01:02:03.001-07:00\"" 115 | "supports #inst literals used alongside datetime literal") 116 | (parsable? 117 | "select foo.* from foo 118 | where product.prod-id between 4000 and 6000 and 119 | product.category <> :product.category/action" 120 | "supports namespaced keyword literals as value in where clause") 121 | (parsable? 122 | "select foo.bar from foo where product.tag = :alabama-exorcist-family" 123 | "supports non-namespaced keyword literals as value in where") 124 | (parsable? 125 | "select foo.bar from foo where 126 | product.uuid between 127 | #uuid \"576073c3-24c5-4461-9b84-dfe65774d41b\" 128 | and #uuid \"5760745a-5bb5-4768-96f7-0f8aeb1a84f0\"" 129 | "supports #uuid literals") 130 | (parsable? 131 | "select foo.bar from foo where 132 | product.url = #uri \"http://example.com/products/2290\"" 133 | "supports #uri literals") 134 | (parsable? 135 | "select foo.bar, #bytes \"QURBUFRBVElPTiBVTlRPVUNIQUJMRVM=\" 136 | from foo where product.prod-id between 2000 and 3000" 137 | "supports #bytes (byte array) literals in select list") 138 | (parsable? 139 | "select foo.bar from foo where 140 | #attr :product/url = #uri \"http://example.com/products/2290\" 141 | and #attr :product/category = :product.category/documentary" 142 | "supports namespaced keyword as column (attr) in where clause") 143 | (is (= (prs/parser 144 | "select #attr :foo/bar from foo 145 | where #attr :foo/quux = :xyzzy.baz/foo") 146 | [:sql_data_statement 147 | [:select_statement 148 | [:select_list [:column_name "foo" "bar"]] 149 | [:from_clause [:table_ref [:table_name "foo"]]] 150 | [:where_clause 151 | [:search_condition 152 | [:boolean_term 153 | [:boolean_factor 154 | [:boolean_test 155 | [:binary_comparison 156 | [:column_name "foo" "quux"] 157 | "=" 158 | [:keyword_literal "xyzzy.baz/foo"]]]]]]]]]) 159 | "ns-keyword in select list maps to column_name") 160 | (parsable? "select foo.bar from product 161 | where #attr :db/id = 17592186045445" 162 | "supports :db/id") 163 | (is (= (prs/parser 164 | "select 42, 1234N, -12, -69N, 3.14159, 6.626E34, 1e-2, 2.7182M, 165 | 1.6182F, #float 99.99999 166 | from foobar") 167 | [:sql_data_statement 168 | [:select_statement 169 | [:select_list 170 | [:long_literal "42"] 171 | [:bigint_literal "1234N"] 172 | [:long_literal "-12"] 173 | [:bigint_literal "-69N"] 174 | [:double_literal "3.14159"] 175 | [:double_literal "6.626E34"] 176 | [:double_literal "1e-2"] 177 | [:bigdec_literal "2.7182M"] 178 | [:float_literal "1.6182F"] 179 | [:float_literal "99.99999"]] 180 | [:from_clause [:table_ref [:table_name "foobar"]]]]]) 181 | "supports long, float, double, bigint, bigdec literals") 182 | (parsable? "select where #attr :product/prod-id between 1567 and 6000" 183 | "allow shortened where-only select statement") 184 | (parsable? 185 | "select #attr :product/title 186 | from product 187 | where #attr :product/actor in ( 188 | 'GENE WILLIS', 'RIP DOUGLAS', 'KIM RYDER')" 189 | "supports IN clauses") 190 | (parsable? 191 | "select where 192 | #attr :product/rating > 2.5f 193 | or (#attr :product/category = :product.category/new 194 | and #attr :product/prod-id < 5000) 195 | and #attr :product/price > 22.0M 196 | or #attr :product/prod-id between 6000 and 7000" 197 | "supports nested AND-OR trees in WHERE") 198 | (parsable? 199 | "select where 200 | #attr :product/rating > 2.5f 201 | or (#attr :product/category = :product.category/new 202 | or (not ( 203 | (#attr :product/prod-id < 5000 204 | or (not #attr :product/category in ( 205 | :product.category/action, 206 | :product.category/comedy 207 | )) 208 | or #attr :product/price > 20.0M 209 | and #attr :product/prod-id >= 2000))))" 210 | "supports nested AND-OR-NOT trees in WHERE")) 211 | 212 | (testing "INSERT statements" 213 | (parsable? 214 | "insert into customers ( 215 | firstname, lastname, address1, address2, 216 | city, state, zip, country 217 | ) values ( 218 | 'Foo', 'Bar', '123 Some Place', '', 219 | 'Thousand Oaks', 'CA', '91362', 'USA' 220 | ) 221 | ") 222 | (parsable? 223 | "INSERT INTO 224 | foo 225 | (name, age, balance, joined_on) 226 | VALUES 227 | ('foo', 42, 1234.56, date '2016-04-01')" 228 | "allows string, integral, float, and date literals as values") 229 | (parsable? 230 | "insert into 231 | #attr :product/prod-id = 9999, 232 | #attr :product/actor = 'Naomi Watts', 233 | #attr :product/title = 'The Ring', 234 | #attr :product/category = :product.category/horror, 235 | #attr :product/rating = 4.5f, 236 | #attr :product/man-hours = 9001N, 237 | #attr :product/price = 21.99M") 238 | "allows shortened assignment-pairs version of insert") 239 | 240 | (testing "UPDATE statements" 241 | (parsable? 242 | "update customers 243 | set customers.city = 'Springfield' 244 | , customers.state = 'VA' 245 | , customers.zip = '22150' 246 | where customers.id = 123454321") 247 | (parsable? 248 | "update employees 249 | set employees.salary = 40000.00 250 | where employees.salary < 38000.00 251 | and employees.hired_on < date '2010-01-01'") 252 | (parsable? 253 | "update #attr :product/rating = 3.5f 254 | where #attr :product/prod-id = 1567" 255 | "allow SET and table to be optional; requires WHERE") 256 | (parsable? 257 | "update set #attr :product/rating = 2.4F 258 | where #attr :product/prod-id = 1567" 259 | "allow table to be optional; requires WHERE")) 260 | 261 | (testing "DELETE statements" 262 | (parsable? 263 | "delete from products where products.actor = 'homer simpson'") 264 | (parsable? 265 | "delete from drop_bear_attacks" 266 | "allows where clause to be optional") 267 | (parsable? 268 | " 269 | DELETE FROM 270 | lineitems 271 | WHERE 272 | lineitems.user_id = 42 273 | AND lineitems.created_at BETWEEN 274 | DATETIME '2013-11-15T00:00:00' 275 | AND 276 | DATETIME '2014-01-15T08:00:00' 277 | ") 278 | (parsable? "delete where #attr :product/prod-id = 1567" 279 | "allow shortened where-only form")) 280 | 281 | (testing "RETRACT statements" 282 | (parsable? "RETRACT product.uuid WHERE db.id = 12345") 283 | (parsable? 284 | "retract #attr :product/actor, 285 | #attr :product/rating, 286 | #attr :product/url 287 | where db.id 12345 54321 42") 288 | (parsable? 289 | "retract #attr :customer/email, 290 | #attr :customer/zip, 291 | #attr :customer/firstname 292 | where db.id in (11111 22222 33333)"))) 293 | 294 | (deftest transform-tests 295 | (testing "SELECT AST -> IR" 296 | (is (= (prs/transform 297 | [:sql_data_statement 298 | [:select_statement 299 | [:select_list 300 | [:qualified_asterisk "a_table"] 301 | [:column_name "b_table" "zebra_id"]] 302 | [:from_clause 303 | [:table_ref [:table_name "a_table"]] 304 | [:table_ref [:table_name "b_table"]]] 305 | [:where_clause 306 | [:search_condition 307 | [:boolean_term 308 | [:boolean_term 309 | [:boolean_term 310 | [:boolean_factor 311 | [:boolean_test 312 | [:binary_comparison 313 | [:column_name "a_table" "id"] 314 | "=" 315 | [:column_name "b_table" "id"]]]]] 316 | [:boolean_factor 317 | [:boolean_test 318 | [:binary_comparison 319 | [:column_name "b_table" "zebra_id"] 320 | ">" 321 | [:long_literal "9000"]]]]] 322 | [:boolean_factor 323 | [:boolean_test 324 | [:binary_comparison 325 | [:column_name "b_table" "hired_on"] 326 | "<" 327 | [:date_literal "2011-11-11"]]]]]]]]]) 328 | {:type :select 329 | :fields [:a_table/* 330 | {:table "b_table" :column "zebra_id"}] 331 | :tables [{:name "a_table"} {:name "b_table"}] 332 | :where [(list := 333 | {:table "a_table" :column "id"} 334 | {:table "b_table" :column "id"}) 335 | (list :> 336 | {:table "b_table" :column "zebra_id"} 337 | 9000) 338 | (list :< 339 | {:table "b_table" :column "hired_on"} 340 | (->> (tm/date-time 2011 11 11) 341 | str 342 | inst/read-instant-date))] 343 | })) 344 | 345 | ;; select product.prod-id from product where product.prod-id between 1 and 2 and product.title <> 'foo' 346 | (is (= (prs/transform 347 | [:sql_data_statement 348 | [:select_statement 349 | [:select_list [:column_name "product" "prod-id"]] 350 | [:from_clause [:table_ref [:table_name "product"]]] 351 | [:where_clause 352 | [:search_condition 353 | [:boolean_term 354 | [:boolean_term 355 | [:boolean_factor 356 | [:boolean_test 357 | [:between_clause 358 | [:column_name "product" "prod-id"] 359 | [:long_literal "1"] 360 | [:long_literal "10"]]]]] 361 | [:boolean_factor 362 | [:boolean_test 363 | [:binary_comparison 364 | [:column_name "product" "title"] 365 | "<>" 366 | [:string_literal "'foo'"]]]]]]]]]) 367 | {:type :select, 368 | :fields [{:table "product", :column "prod-id"}], 369 | :tables [{:name "product"}], 370 | :where 371 | '[(:between {:table "product", :column "prod-id"} 1 10) 372 | (:not= {:table "product", :column "title"} "foo")]})) 373 | 374 | ;; select foo.* from foo where product.prod-id between 4000 and 6000 and product.category <> :product.category/action 375 | (is (= (prs/transform 376 | [:sql_data_statement 377 | [:select_statement 378 | [:select_list [:qualified_asterisk "foo"]] 379 | [:from_clause [:table_ref [:table_name "foo"]]] 380 | [:where_clause 381 | [:search_condition 382 | [:boolean_term 383 | [:boolean_term 384 | [:boolean_factor 385 | [:boolean_test 386 | [:between_clause 387 | [:column_name "product" "prod-id"] 388 | [:long_literal "4000"] 389 | [:long_literal "6000"]]]]] 390 | [:boolean_factor 391 | [:boolean_test 392 | [:binary_comparison 393 | [:column_name "product" "category"] 394 | "<>" 395 | [:keyword_literal "product.category/action"]]]]]]]]]) 396 | {:type :select, 397 | :fields [:foo/*], 398 | :tables [{:name "foo"}], 399 | :where 400 | '[(:between {:table "product", :column "prod-id"} 4000 6000) 401 | (:not= 402 | {:table "product", :column "category"} 403 | :product.category/action)]})) 404 | 405 | ;; select foo.bar from foo where product.tag = :alabama-exorcist-family 406 | (is (prs/transform 407 | [:sql_data_statement 408 | [:select_statement 409 | [:select_list [:column_name "foo" "bar"]] 410 | [:from_clause [:table_ref [:table_name "foo"]]] 411 | [:where_clause 412 | [:search_condition 413 | [:boolean_term 414 | [:boolean_factor 415 | [:boolean_test 416 | [:binary_comparison 417 | [:column_name "product" "tag"] 418 | "=" 419 | [:keyword_literal "alabama-exorcist-family"]]]]]]]]]) 420 | {:type :select, 421 | :fields [{:table "foo", :column "bar"}], 422 | :tables [{:name "foo"}], 423 | :where 424 | '[(:= {:table "product", :column "tag"} :alabama-exorcist-family)]}) 425 | 426 | ;; select foo.bar from foo where product.uuid between #uuid "576073c3-24c5-4461-9b84-dfe65774d41b" and #uuid "5760745a-5bb5-4768-96f7-0f8aeb1a84f0" 427 | (is (prs/transform 428 | [:sql_data_statement 429 | [:select_statement 430 | [:select_list [:column_name "foo" "bar"]] 431 | [:from_clause [:table_ref [:table_name "foo"]]] 432 | [:where_clause 433 | [:search_condition 434 | [:boolean_term 435 | [:boolean_factor 436 | [:boolean_test 437 | [:between_clause 438 | [:column_name "product" "uuid"] 439 | [:uuid_literal "576073c3-24c5-4461-9b84-dfe65774d41b"] 440 | [:uuid_literal "5760745a-5bb5-4768-96f7-0f8aeb1a84f0"]]]]]]]]]) 441 | {:type :select, 442 | :fields [{:table "foo", :column "bar"}], 443 | :tables [{:name "foo"}], 444 | :where 445 | '[(:between 446 | {:table "product", :column "uuid"} 447 | #uuid "576073c3-24c5-4461-9b84-dfe65774d41b" 448 | #uuid "5760745a-5bb5-4768-96f7-0f8aeb1a84f0")]}) 449 | 450 | ;; select foo.bar from foo where product.url = #uri "http://example.com/products/2290" 451 | (is (prs/transform 452 | [:sql_data_statement 453 | [:select_statement 454 | [:select_list [:column_name "foo" "bar"]] 455 | [:from_clause [:table_ref [:table_name "foo"]]] 456 | [:where_clause 457 | [:search_condition 458 | [:boolean_term 459 | [:boolean_factor 460 | [:boolean_test 461 | [:binary_comparison 462 | [:column_name "product" "url"] 463 | "=" 464 | [:uri_literal "http://example.com/products/2290"]]]]]]]]]) 465 | {:type :select, 466 | :fields [{:table "foo", :column "bar"}], 467 | :tables [{:name "foo"}], 468 | :where 469 | '[(:= 470 | {:table "product", :column "url"} 471 | #uri "http://example.com/products/2290")]}) 472 | 473 | ;; select foo.bar, #bytes "QURBUFRBVElPTiBVTlRPVUNIQUJMRVM=" from foo where product.prod-id between 2000 and 3000 474 | (is (prs/transform 475 | [:sql_data_statement 476 | [:select_statement 477 | [:select_list 478 | [:column_name "foo" "bar"] 479 | [:bytes_literal "QURBUFRBVElPTiBVTlRPVUNIQUJMRVM="]] 480 | [:from_clause [:table_ref [:table_name "foo"]]] 481 | [:where_clause 482 | [:search_condition 483 | [:boolean_term 484 | [:boolean_factor 485 | [:boolean_test 486 | [:between_clause 487 | [:column_name "product" "prod-id"] 488 | [:long_literal "2000"] 489 | [:long_literal "3000"]]]]]]]]]) 490 | {:type :select 491 | :fields [{:table "foo" :column "bar"} 492 | [65 68 65 80 84 65 84 73 79 78 32 85 78 84 79 85 67 493 | 72 65 66 76 69 83]] 494 | :tables [{:name "foo"}] 495 | :where ['(:between {:table "product" :column "prod-id"} 496 | 2000 3000)]}) 497 | 498 | ;; select foo.bar from product where #attr :db/id = 17592186045445 499 | (is (= (prs/transform 500 | [:sql_data_statement 501 | [:select_statement 502 | [:select_list [:column_name "foo" "bar"]] 503 | [:from_clause [:table_ref [:table_name "product"]]] 504 | [:where_clause 505 | [:db_id_clause [:long_literal "17592186045445"]]]]]) 506 | {:type :select, 507 | :fields [{:table "foo", :column "bar"}], 508 | :tables [{:name "product"}], 509 | :where [#{17592186045445}]}) 510 | "`:db/id = eid` translates to :db-id clause, not :binary_comparison") 511 | 512 | ;; update product set #attr :product/category = :product.category/new, #attr :product/tag = :foo-bar-baz, #attr :product/actor = 'Quux the Great' where #attr :product/prod-id = 1567 513 | (is (= (prs/transform 514 | [:sql_data_statement 515 | [:update_statement 516 | [:table_name "product"] 517 | [:set_clausen 518 | [:assignment_pair 519 | [:column_name "product" "category"] 520 | [:keyword_literal "product.category/new"]] 521 | [:assignment_pair 522 | [:column_name "product" "tag"] 523 | [:keyword_literal "foo-bar-baz"]] 524 | [:assignment_pair 525 | [:column_name "product" "actor"] 526 | [:string_literal "'Quux the Great'"]]] 527 | [:where_clause 528 | [:search_condition 529 | [:boolean_term 530 | [:boolean_factor 531 | [:boolean_test 532 | [:binary_comparison 533 | [:column_name "product" "prod-id"] 534 | "=" 535 | [:long_literal "1567"]]]]]]]]]) 536 | {:type :update, 537 | :table "product", 538 | :assign-pairs 539 | [[{:table "product", :column "category"} :product.category/new] 540 | [{:table "product", :column "tag"} :foo-bar-baz] 541 | [{:table "product", :column "actor"} "Quux the Great"]], 542 | :where ['(:= {:table "product", :column "prod-id"} 1567)]})) 543 | 544 | ;; delete from product where #attr :product/prod-id between 2000 and 4000 545 | (is (= (prs/transform 546 | [:sql_data_statement 547 | [:delete_statement 548 | [:table_name "product"] 549 | [:where_clause 550 | [:search_condition 551 | [:boolean_term 552 | [:boolean_factor 553 | [:boolean_test 554 | [:between_clause 555 | [:column_name "product" "prod-id"] 556 | [:long_literal "2000"] 557 | [:long_literal "4000"]]]]]]]]]) 558 | {:type :delete, 559 | :table "product", 560 | :where ['(:between {:table "product", :column "prod-id"} 561 | 2000 4000)]})) 562 | 563 | ;; insert into product (prod-id, actor, title, category) values (9999, 'Naomi Watts', 'The Ring', :product.category/horror) 564 | (is (= (prs/transform 565 | [:sql_data_statement 566 | [:insert_statement 567 | [:table_name "product"] 568 | [:insert_cols "prod-id" "actor" "title" "category"] 569 | [:insert_vals 570 | [:long_literal "9999"] 571 | [:string_literal "'Naomi Watts'"] 572 | [:string_literal "'The Ring'"] 573 | [:keyword_literal "product.category/horror"]]]]) 574 | {:type :insert, 575 | :table "product", 576 | :cols ["prod-id" "actor" "title" "category"], 577 | :vals [9999 "Naomi Watts" "The Ring" :product.category/horror]})) 578 | 579 | ;; select 42, 1234N, -12, -69N, 3.14159, 6.626E34, 1e-2, 2.7182M, 1.6182F from foobar 580 | (is (= (prs/transform 581 | [:sql_data_statement 582 | [:select_statement 583 | [:select_list 584 | [:long_literal "42"] 585 | [:bigint_literal "1234N"] 586 | [:long_literal "-12"] 587 | [:bigint_literal "-69N"] 588 | [:double_literal "3.14159"] 589 | [:double_literal "6.626E34"] 590 | [:double_literal "1e-2"] 591 | [:bigdec_literal "2.7182M"] 592 | [:float_literal "1.6182F"] 593 | ] 594 | [:from_clause [:table_ref [:table_name "foobar"]]]]]) 595 | {:type :select 596 | :fields [42 597 | 1234N 598 | -12 599 | -69N 600 | 3.14159 601 | 6.626E34 602 | 0.01 603 | 2.7182M 604 | (float 1.6182) 605 | ] 606 | :tables [{:name "foobar"}]})) 607 | 608 | ;; select where #attr :product/prod-id between 1567 and 6000 609 | (is (= (prs/transform 610 | [:sql_data_statement 611 | [:select_statement 612 | [:where_clause 613 | [:search_condition 614 | [:boolean_term 615 | [:boolean_factor 616 | [:boolean_test 617 | [:between_clause 618 | [:column_name "product" "prod-id"] 619 | [:long_literal "1567"] 620 | [:long_literal "6000"]]]]]]]]]) 621 | {:type :select 622 | :where ['(:between {:table "product", :column "prod-id"} 623 | 1567 6000)]})) 624 | 625 | ;; insert into #attr :product/prod-id = 9999, #attr :product/actor = 'Naomi Watts', #attr :product/title = 'The Ring', #attr :product/category = :product.category/horror, #attr :product/rating = 4.5f, #attr :product/man-hours = 9001N, #attr :product/price = 21.99M 626 | (is (= (prs/transform 627 | [:sql_data_statement 628 | [:insert_statement 629 | [:set_clausen 630 | [:assignment_pair 631 | [:column_name "product" "prod-id"] 632 | [:long_literal "9999"]] 633 | [:assignment_pair 634 | [:column_name "product" "actor"] 635 | [:string_literal "'Naomi Watts'"]] 636 | [:assignment_pair 637 | [:column_name "product" "title"] 638 | [:string_literal "'The Ring'"]] 639 | [:assignment_pair 640 | [:column_name "product" "category"] 641 | [:keyword_literal "product.category/horror"]] 642 | [:assignment_pair 643 | [:column_name "product" "rating"] 644 | [:float_literal "4.5f"]] 645 | [:assignment_pair 646 | [:column_name "product" "man-hours"] 647 | [:bigint_literal "9001N"]] 648 | [:assignment_pair 649 | [:column_name "product" "price"] 650 | [:bigdec_literal "21.99M"]]]]]) 651 | {:type :insert, 652 | :assign-pairs 653 | [[{:table "product", :column "prod-id"} 9999] 654 | [{:table "product", :column "actor"} "Naomi Watts"] 655 | [{:table "product", :column "title"} "The Ring"] 656 | [{:table "product", :column "category"} 657 | :product.category/horror] 658 | [{:table "product", :column "rating"} #float 4.5] 659 | [{:table "product", :column "man-hours"} 9001N] 660 | [{:table "product", :column "price"} 21.99M]]})) 661 | 662 | ;; delete where #attr :product/prod-id between 1567 and 6000 663 | (is (= (prs/transform 664 | [:sql_data_statement 665 | [:delete_statement 666 | [:where_clause 667 | [:search_condition 668 | [:boolean_term 669 | [:boolean_factor 670 | [:boolean_test 671 | [:between_clause 672 | [:column_name "product" "prod-id"] 673 | [:long_literal "1567"] 674 | [:long_literal "6000"]]]]]]]]]) 675 | {:type :delete 676 | :where ['(:between {:table "product", :column "prod-id"} 677 | 1567 6000)]})) 678 | 679 | ;; update #attr :product/rating = 3.5f where #attr :product/prod-id = 1567 680 | (is (= (prs/transform 681 | [:sql_data_statement 682 | [:update_statement 683 | [:set_clausen 684 | [:assignment_pair 685 | [:column_name "product" "rating"] 686 | [:float_literal "3.5f"]]] 687 | [:where_clause 688 | [:search_condition 689 | [:boolean_term 690 | [:boolean_factor 691 | [:boolean_test 692 | [:binary_comparison 693 | [:column_name "product" "prod-id"] 694 | "=" 695 | [:long_literal "1567"]]]]]]]]]) 696 | {:type :update 697 | :assign-pairs [[{:table "product", :column "rating"} 698 | #float 3.5]] 699 | :where ['(:= {:table "product", :column "prod-id"} 1567)]})) 700 | 701 | ;; select #attr :product/title from product where #attr :product/actor in ('GENE WILLIS', 'RIP DOUGLAS', 'KIM RYDER') 702 | (is (= (prs/transform 703 | [:sql_data_statement 704 | [:select_statement 705 | [:select_list [:column_name "product" "title"]] 706 | [:from_clause [:table_ref [:table_name "product"]]] 707 | [:where_clause 708 | [:search_condition 709 | [:boolean_term 710 | [:boolean_factor 711 | [:boolean_test 712 | [:in_clause 713 | [:column_name "product" "actor"] 714 | [:string_literal "'GENE WILLIS'"] 715 | [:string_literal "'RIP DOUGLAS'"] 716 | [:string_literal "'KIM RYDER'"]]]]]]]]]) 717 | {:type :select 718 | :fields [{:table "product", :column "title"}] 719 | :tables [{:name "product"}] 720 | :where 721 | ['(:in 722 | {:table "product", :column "actor"} 723 | ["GENE WILLIS" "RIP DOUGLAS" "KIM RYDER"])]})) 724 | 725 | ;; select where #attr :product/rating > 2.5f or (#attr :product/category = :product.category/new and #attr :product/prod-id < 5000) and #attr :product/price > 22.0M or #attr :product/prod-id between 6000 and 7000 726 | (is (= (prs/transform 727 | [:sql_data_statement 728 | [:select_statement 729 | [:where_clause 730 | [:search_condition 731 | [:search_condition 732 | [:search_condition 733 | [:boolean_term 734 | [:boolean_factor 735 | [:boolean_test 736 | [:binary_comparison 737 | [:column_name "product" "rating"] 738 | ">" 739 | [:float_literal "2.5f"]]]]]] 740 | [:boolean_term 741 | [:boolean_term 742 | [:boolean_factor 743 | [:boolean_test 744 | [:search_condition 745 | [:boolean_term 746 | [:boolean_term 747 | [:boolean_factor 748 | [:boolean_test 749 | [:binary_comparison 750 | [:column_name "product" "category"] 751 | "=" 752 | [:keyword_literal "product.category/new"]]]]] 753 | [:boolean_factor 754 | [:boolean_test 755 | [:binary_comparison 756 | [:column_name "product" "prod-id"] 757 | "<" 758 | [:long_literal "5000"]]]]]]]]] 759 | [:boolean_factor 760 | [:boolean_test 761 | [:binary_comparison 762 | [:column_name "product" "price"] 763 | ">" 764 | [:bigdec_literal "22.0M"]]]]]] 765 | [:boolean_term 766 | [:boolean_factor 767 | [:boolean_test 768 | [:between_clause 769 | [:column_name "product" "prod-id"] 770 | [:long_literal "6000"] 771 | [:long_literal "7000"]]]]]]]]]) 772 | {:type :select, 773 | :where 774 | '[(:or 775 | (:> {:table "product", :column "rating"} #float 2.5) 776 | (:and 777 | (:= {:table "product", :column "category"} 778 | :product.category/new) 779 | (:< {:table "product", :column "prod-id"} 5000) 780 | (:> {:table "product", :column "price"} 22.0M)) 781 | (:between {:table "product", :column "prod-id"} 782 | 6000 7000))]})) 783 | 784 | ;; select where #attr :product/rating > 2.5f or (#attr :product/category = :product.category/new or (not ( (#attr :product/prod-id < 5000 or (not #attr :product/category in (:product.category/action, :product.category/comedy)) or #attr :product/price > 20.0M) and #attr :product/prod-id >= 2000))) 785 | (is (= (prs/transform 786 | [:sql_data_statement 787 | [:select_statement 788 | [:where_clause 789 | [:search_condition 790 | [:search_condition 791 | [:boolean_term 792 | [:boolean_factor 793 | [:boolean_test 794 | [:binary_comparison 795 | [:column_name "product" "rating"] 796 | ">" 797 | [:float_literal "2.5f"]]]]]] 798 | [:boolean_term 799 | [:boolean_factor 800 | [:boolean_test 801 | [:search_condition 802 | [:search_condition 803 | [:boolean_term 804 | [:boolean_factor 805 | [:boolean_test 806 | [:binary_comparison 807 | [:column_name "product" "category"] 808 | "=" 809 | [:keyword_literal "product.category/new"]]]]]] 810 | [:boolean_term 811 | [:boolean_factor 812 | [:boolean_test 813 | [:search_condition 814 | [:boolean_term 815 | [:boolean_factor 816 | [:boolean_negative 817 | [:boolean_test 818 | [:search_condition 819 | [:boolean_term 820 | [:boolean_term 821 | [:boolean_factor 822 | [:boolean_test 823 | [:search_condition 824 | [:search_condition 825 | [:search_condition 826 | [:boolean_term 827 | [:boolean_factor 828 | [:boolean_test 829 | [:binary_comparison 830 | [:column_name "product" "prod-id"] 831 | "<" 832 | [:long_literal "5000"]]]]]] 833 | [:boolean_term 834 | [:boolean_factor 835 | [:boolean_test 836 | [:search_condition 837 | [:boolean_term 838 | [:boolean_factor 839 | [:boolean_negative 840 | [:boolean_test 841 | [:in_clause 842 | [:column_name "product" "category"] 843 | [:keyword_literal 844 | "product.category/action"] 845 | [:keyword_literal 846 | "product.category/comedy"]]]]]]]]]]] 847 | [:boolean_term 848 | [:boolean_factor 849 | [:boolean_test 850 | [:binary_comparison 851 | [:column_name "product" "price"] 852 | ">" 853 | [:bigdec_literal "20.0M"]]]]]]]]] 854 | [:boolean_factor 855 | [:boolean_test 856 | [:binary_comparison 857 | [:column_name "product" "prod-id"] 858 | ">=" 859 | [:long_literal "2000"]]]]]]]]]]]]]]]]]]]]]]) 860 | {:type :select 861 | :where 862 | '[(:or 863 | (:> {:table "product", :column "rating"} #float 2.5) 864 | (:or 865 | (:= {:table "product", :column "category"} 866 | :product.category/new) 867 | (:not 868 | (:and 869 | (:or 870 | (:< {:table "product", :column "prod-id"} 5000) 871 | (:not 872 | (:in 873 | {:table "product", :column "category"} 874 | [:product.category/action :product.category/comedy])) 875 | (:> {:table "product", :column "price"} 20.0M)) 876 | (:>= {:table "product", :column "prod-id"} 2000)))))]})) 877 | 878 | ;; retract #attr :product/actor, #attr :product/rating, #attr :product/url where db.id 12345 54321 42 879 | (is (= (prs/transform 880 | [:sql_data_statement 881 | [:retract_statement 882 | [:retract_attrs 883 | [:column_name "product" "actor"] 884 | [:column_name "product" "rating"] 885 | [:column_name "product" "url"]] 886 | [:db_id_clause 887 | [:long_literal "12345"] 888 | [:long_literal "54321"] 889 | [:long_literal "42"]]]]) 890 | {:type :retract 891 | :ids #{12345 54321 42} 892 | :attrs 893 | [{:table "product" :column "actor"} 894 | {:table "product" :column "rating"} 895 | {:table "product" :column "url"}]})) 896 | )) 897 | --------------------------------------------------------------------------------