├── docs ├── _config.yml ├── css │ ├── highlight.css │ └── default.css ├── js │ ├── page_effects.js │ ├── highlight.min.js │ └── jquery.min.js ├── index.html ├── org.purefn.sqlium.html └── org.purefn.sqlium.import.html ├── dev ├── user.clj └── dev.clj ├── .gitignore ├── project.clj ├── src └── org │ └── purefn │ ├── sqlium │ ├── dsl.clj │ ├── dsl │ │ ├── parse.clj │ │ └── analyze.clj │ ├── transform.clj │ ├── sql.clj │ └── import.clj │ └── sqlium.clj ├── LICENSE └── README.md /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [clojure.tools.namespace.repl :refer :all])) 3 | 4 | (defn dev 5 | [] 6 | (require 'dev :reload) 7 | (in-ns 'dev)) 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /dev/dev.clj: -------------------------------------------------------------------------------- 1 | (ns dev 2 | (:require [clojure.java.io :as io] 3 | [clojure.java.jdbc :as jdbc] 4 | [clojure.pprint :refer (pprint)] 5 | [clojure.repl :refer :all] 6 | [clojure.set :as set] 7 | [clojure.string :as str] 8 | [clojure.tools.namespace.repl :refer :all] 9 | 10 | [org.purefn.sqlium :as sqlium])) 11 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.purefn/sqlium "0.1.5-SNAPSHOT" 2 | :description "A flexible, Datomic-inspired, config-driven Extraction system (big E in ETL)." 3 | :url "http://github.com/TheLadders/sqlium" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [org.clojure/java.jdbc "0.6.1"] 8 | [org.clojure/tools.logging "0.4.0"] 9 | [clj-time "0.13.0"]] 10 | :deploy-repositories [["releases" :clojars] 11 | ["snapshots" :clojars]] 12 | :profiles {:dev {:dependencies [[org.clojure/tools.namespace "0.2.11"] 13 | [mysql/mysql-connector-java "5.1.6"] 14 | [org.postgresql/postgresql "42.1.1"]] 15 | :plugins [[lein-codox "0.10.3"]] 16 | :codox {:namespaces [org.purefn.sqlium org.purefn.sqlium.import] 17 | :output-path "docs"} 18 | :source-paths ["dev"]}}) 19 | -------------------------------------------------------------------------------- /docs/css/highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | github.com style (c) Vasily Polovnyov 3 | */ 4 | 5 | .hljs { 6 | display: block; 7 | overflow-x: auto; 8 | padding: 0.5em; 9 | color: #333; 10 | background: #f8f8f8; 11 | } 12 | 13 | .hljs-comment, 14 | .hljs-quote { 15 | color: #998; 16 | font-style: italic; 17 | } 18 | 19 | .hljs-keyword, 20 | .hljs-selector-tag, 21 | .hljs-subst { 22 | color: #333; 23 | font-weight: bold; 24 | } 25 | 26 | .hljs-number, 27 | .hljs-literal, 28 | .hljs-variable, 29 | .hljs-template-variable, 30 | .hljs-tag .hljs-attr { 31 | color: #008080; 32 | } 33 | 34 | .hljs-string, 35 | .hljs-doctag { 36 | color: #d14; 37 | } 38 | 39 | .hljs-title, 40 | .hljs-section, 41 | .hljs-selector-id { 42 | color: #900; 43 | font-weight: bold; 44 | } 45 | 46 | .hljs-subst { 47 | font-weight: normal; 48 | } 49 | 50 | .hljs-type, 51 | .hljs-class .hljs-title { 52 | color: #458; 53 | font-weight: bold; 54 | } 55 | 56 | .hljs-tag, 57 | .hljs-name, 58 | .hljs-attribute { 59 | color: #000080; 60 | font-weight: normal; 61 | } 62 | 63 | .hljs-regexp, 64 | .hljs-link { 65 | color: #009926; 66 | } 67 | 68 | .hljs-symbol, 69 | .hljs-bullet { 70 | color: #990073; 71 | } 72 | 73 | .hljs-built_in, 74 | .hljs-builtin-name { 75 | color: #0086b3; 76 | } 77 | 78 | .hljs-meta { 79 | color: #999; 80 | font-weight: bold; 81 | } 82 | 83 | .hljs-deletion { 84 | background: #fdd; 85 | } 86 | 87 | .hljs-addition { 88 | background: #dfd; 89 | } 90 | 91 | .hljs-emphasis { 92 | font-style: italic; 93 | } 94 | 95 | .hljs-strong { 96 | font-weight: bold; 97 | } 98 | -------------------------------------------------------------------------------- /src/org/purefn/sqlium/dsl.clj: -------------------------------------------------------------------------------- 1 | (ns org.purefn.sqlium.dsl 2 | "Convenience functions for interacting with sqlium's SQL entity DSL. 3 | See the two DSL namespaces for more extensive documentation about 4 | the contents of specs and the return values of the various 5 | operations " 6 | (:require [org.purefn.sqlium.dsl.parse :as parse] 7 | [org.purefn.sqlium.dsl.analyze :as analyze])) 8 | 9 | (defn single-relationships 10 | "Returns the single relationships for an (analyzed) table spec." 11 | [spec] 12 | (get-in spec [:relationships :one])) 13 | 14 | (defn group-tables 15 | "Returns a collection all the tables in group for an (analyzed) 16 | table spec, ie. the table itself and all the single relationships." 17 | [spec] 18 | (->> (single-relationships spec) 19 | (map :target) 20 | (cons spec))) 21 | 22 | (defn relationship? 23 | "True if spec entry is a relationship." 24 | [ent] 25 | (= :relationship (:type ent))) 26 | 27 | (defn id-column 28 | "Returns the id column for a spec as a keyword :table/column." 29 | [spec] 30 | (keyword (name (:name spec)) 31 | (name (:id spec)))) 32 | 33 | (defn parse-spec 34 | "Takes a DSL expression of a spec, returns parsed spec." 35 | [dsl-spec] 36 | (parse/spec dsl-spec)) 37 | 38 | (defn group-spec 39 | "Takes a (parsed) spec, returns structure with the relationships 40 | grouped." 41 | [spec] 42 | (analyze/group-relationships spec)) 43 | 44 | (defn compile-spec* 45 | "Takes a DSL spec, returns map with the parsed spec in :spec key, 46 | and grouped spec in :grouped key." 47 | [dsl-spec] 48 | (let [spec (parse-spec dsl-spec)] 49 | {:spec spec 50 | :grouped (group-spec spec)})) 51 | 52 | (def compile-spec (memoize compile-spec*)) 53 | -------------------------------------------------------------------------------- /src/org/purefn/sqlium.clj: -------------------------------------------------------------------------------- 1 | (ns org.purefn.sqlium 2 | "A flexible, config-driven, Datomic-inspired entity extraction 3 | system. sqlium provides a declarative DSL to describe entities 4 | stored in a relational database - particularly those that are 5 | composed of many tables with nested relationships - and mechanisms 6 | to read and transform them into Clojure datastructures." 7 | (:require [org.purefn.sqlium.dsl :as dsl] 8 | [org.purefn.sqlium.import :as import] 9 | [org.purefn.sqlium.transform :as transform])) 10 | 11 | (defn record-id 12 | "Returns the id of a record." 13 | [record] 14 | (::id (meta record))) 15 | 16 | (defn entity 17 | "Returns a single entity by id for given spec, querying from jdbc 18 | datasource db." 19 | [db spec id] 20 | (let [compiled-spec (dsl/compile-spec spec) 21 | xform (transform/group-transform (:grouped compiled-spec))] 22 | (some-> (import/import-record db compiled-spec id) 23 | xform 24 | (vary-meta assoc ::id id)))) 25 | 26 | (defn records 27 | "Returns a lazy sequence of records for spec, querying from jdbc 28 | datasource db. Takes optional parameters as kwargs or a map to 29 | either control expiry, return updated data since a given date by 30 | comparing against provided date time fields, or return updated data 31 | based on a specific update table. Only the highest-precedence 32 | option present will be used. In order of precedence: 33 | 34 | :update - a map with: 35 | * :table string name of the update table 36 | * :id string name of the column containing entity ids to update 37 | * :updated string name of the field containing the entity update time 38 | * :date anything that can be coerced to a DateTime; the records 39 | returned will be newer than this date 40 | 41 | :delta - a map with: 42 | * :fields collection of :table/column datetime fields which will 43 | be compared with :date to detect updated data 44 | * :date anything that can be coerced to a DateTime; the records 45 | returned will be newer than this date 46 | 47 | :expiry - a map with: 48 | * :field :table/column keyword for the datetime field that 49 | determines the age of the entity 50 | * :age maximum age before the entity is ignored, either as an 51 | integer number of days or an expiration date as something 52 | that can be coerced to a DateTime" 53 | [db spec & options] 54 | (let [opts (if (= 1 (count options)) 55 | (first options) 56 | (apply hash-map options)) 57 | compiled (dsl/compile-spec spec) 58 | xform (transform/group-transform (:grouped compiled)) 59 | id (dsl/id-column (:spec compiled)) 60 | records (import/import-table db compiled opts)] 61 | (with-meta (sequence (map (fn [r] (with-meta (xform r) 62 | {::id (id r)}))) 63 | records) 64 | (meta records)))) 65 | -------------------------------------------------------------------------------- /docs/js/page_effects.js: -------------------------------------------------------------------------------- 1 | function visibleInParent(element) { 2 | var position = $(element).position().top 3 | return position > -50 && position < ($(element).offsetParent().height() - 50) 4 | } 5 | 6 | function hasFragment(link, fragment) { 7 | return $(link).attr("href").indexOf("#" + fragment) != -1 8 | } 9 | 10 | function findLinkByFragment(elements, fragment) { 11 | return $(elements).filter(function(i, e) { return hasFragment(e, fragment)}).first() 12 | } 13 | 14 | function scrollToCurrentVarLink(elements) { 15 | var elements = $(elements); 16 | var parent = elements.offsetParent(); 17 | 18 | if (elements.length == 0) return; 19 | 20 | var top = elements.first().position().top; 21 | var bottom = elements.last().position().top + elements.last().height(); 22 | 23 | if (top >= 0 && bottom <= parent.height()) return; 24 | 25 | if (top < 0) { 26 | parent.scrollTop(parent.scrollTop() + top); 27 | } 28 | else if (bottom > parent.height()) { 29 | parent.scrollTop(parent.scrollTop() + bottom - parent.height()); 30 | } 31 | } 32 | 33 | function setCurrentVarLink() { 34 | $('.secondary a').parent().removeClass('current') 35 | $('.anchor'). 36 | filter(function(index) { return visibleInParent(this) }). 37 | each(function(index, element) { 38 | findLinkByFragment(".secondary a", element.id). 39 | parent(). 40 | addClass('current') 41 | }); 42 | scrollToCurrentVarLink('.secondary .current'); 43 | } 44 | 45 | var hasStorage = (function() { try { return localStorage.getItem } catch(e) {} }()) 46 | 47 | function scrollPositionId(element) { 48 | var directory = window.location.href.replace(/[^\/]+\.html$/, '') 49 | return 'scroll::' + $(element).attr('id') + '::' + directory 50 | } 51 | 52 | function storeScrollPosition(element) { 53 | if (!hasStorage) return; 54 | localStorage.setItem(scrollPositionId(element) + "::x", $(element).scrollLeft()) 55 | localStorage.setItem(scrollPositionId(element) + "::y", $(element).scrollTop()) 56 | } 57 | 58 | function recallScrollPosition(element) { 59 | if (!hasStorage) return; 60 | $(element).scrollLeft(localStorage.getItem(scrollPositionId(element) + "::x")) 61 | $(element).scrollTop(localStorage.getItem(scrollPositionId(element) + "::y")) 62 | } 63 | 64 | function persistScrollPosition(element) { 65 | recallScrollPosition(element) 66 | $(element).scroll(function() { storeScrollPosition(element) }) 67 | } 68 | 69 | function sidebarContentWidth(element) { 70 | var widths = $(element).find('.inner').map(function() { return $(this).innerWidth() }) 71 | return Math.max.apply(Math, widths) 72 | } 73 | 74 | function calculateSize(width, snap, margin, minimum) { 75 | if (width == 0) { 76 | return 0 77 | } 78 | else { 79 | return Math.max(minimum, (Math.ceil(width / snap) * snap) + (margin * 2)) 80 | } 81 | } 82 | 83 | function resizeSidebars() { 84 | var primaryWidth = sidebarContentWidth('.primary') 85 | var secondaryWidth = 0 86 | 87 | if ($('.secondary').length != 0) { 88 | secondaryWidth = sidebarContentWidth('.secondary') 89 | } 90 | 91 | // snap to grid 92 | primaryWidth = calculateSize(primaryWidth, 32, 13, 160) 93 | secondaryWidth = calculateSize(secondaryWidth, 32, 13, 160) 94 | 95 | $('.primary').css('width', primaryWidth) 96 | $('.secondary').css('width', secondaryWidth).css('left', primaryWidth + 1) 97 | 98 | if (secondaryWidth > 0) { 99 | $('#content').css('left', primaryWidth + secondaryWidth + 2) 100 | } 101 | else { 102 | $('#content').css('left', primaryWidth + 1) 103 | } 104 | } 105 | 106 | $(window).ready(resizeSidebars) 107 | $(window).ready(setCurrentVarLink) 108 | $(window).ready(function() { persistScrollPosition('.primary')}) 109 | $(window).ready(function() { 110 | $('#content').scroll(setCurrentVarLink) 111 | $(window).resize(setCurrentVarLink) 112 | }) 113 | -------------------------------------------------------------------------------- /src/org/purefn/sqlium/dsl/parse.clj: -------------------------------------------------------------------------------- 1 | (ns org.purefn.sqlium.dsl.parse) 2 | 3 | ;; TODO: rewrite spec and internal representation examples 4 | 5 | (declare spec) 6 | 7 | (defn- true-keys 8 | "Adds keys from collection ks to map m with true vals." 9 | [m ks] 10 | (->> ks 11 | (map (juxt identity (constantly true))) 12 | (into m))) 13 | 14 | (defn- speclist-map 15 | "Takes a speclist (defined as list of keywords possibly followed by 16 | one or more values, repeated), returns a map where keywords with no 17 | value immediately following are keys with true values, and keywords 18 | followed by non-keywords become normal map entries." 19 | [specs] 20 | (let [kvs (->> specs 21 | (partition-by keyword?) 22 | (partition-all 2))] 23 | (reduce (fn [m [ks v]] 24 | (let [[v-k & empty-ks :as all] (reverse ks) 25 | empty-ks (if v 26 | empty-ks 27 | all)] 28 | (cond-> m 29 | v (assoc v-k v) 30 | (seq empty-ks) (true-keys empty-ks)))) 31 | {} 32 | kvs))) 33 | 34 | (defn- maybe-generate-id 35 | "Takes a table spec (with minimum :name and optional :id field) and, 36 | if id field is not present, generates a default one from the table 37 | name. Returns updated table spec. 38 | 39 | Generated ids are in the form of \"{table-name}_id\"" 40 | [table-spec] 41 | (if (:id table-spec) 42 | table-spec 43 | (let [table-name (:name table-spec) 44 | generated-id (str table-name "_id")] 45 | (assoc table-spec 46 | :id generated-id 47 | :id-generated? true)))) 48 | 49 | (defn table 50 | [[_ table-sym & table-spec]] 51 | (assert (keyword? (first table-spec))) 52 | (let [{:keys [id fields]} (speclist-map table-spec) 53 | base-table (maybe-generate-id {:type :table 54 | :name (name table-sym) 55 | :id (first id)})] 56 | (cond-> base-table 57 | (seq fields) 58 | (assoc :fields (map (partial spec base-table) fields))))) 59 | 60 | (defn string-field 61 | [s for-table] 62 | {:type :field 63 | :column (keyword s)}) 64 | 65 | (defn field-spec 66 | [[field & opts] for-table] 67 | (let [xform? (some-fn symbol? list? (partial instance? clojure.lang.Cons)) 68 | {:keys [as] :as opts-m} (speclist-map (remove xform? opts)) 69 | xform (first (filter xform? opts))] 70 | (-> opts-m 71 | (dissoc :as) 72 | (merge {:type :field 73 | :column (keyword field)}) 74 | (cond-> 75 | as (assoc :alias (first as)) 76 | xform (assoc :transform xform))))) 77 | 78 | (defn relationship 79 | [rel-m for-table] 80 | (let [;; since relationships are maps where one keyval is the 81 | ;; relationhsip def, and the other keys are other keyword 82 | ;; params, we need to separate out the relationship def from 83 | ;; the other params. 84 | {[rel-field & invalid?] false other-params true} (group-by keyword? (keys rel-m)) 85 | _ (assert (empty? invalid?) (str "Invalid relationship spec " rel-m)) 86 | table-spec (get rel-m rel-field) 87 | _ (assert (list? table-spec) (str "Invalid table spec " table-spec)) 88 | target-table (table table-spec) 89 | {:keys [column alias flatten] :as field-spec} (spec target-table rel-field) 90 | reverse-ref? (.startsWith (name column) "_") 91 | rel-column (if reverse-ref? 92 | (keyword (:name target-table) (subs (name column) 1)) 93 | (keyword (:name for-table) (name column)))] 94 | (merge (select-keys rel-m other-params) 95 | (select-keys field-spec [:alias :flatten]) 96 | {:type :relationship 97 | :source-table for-table 98 | :column rel-column 99 | :target target-table}))) 100 | 101 | (defn spec 102 | "Takes dsl element x and optionally the spec for the table it belongs to, 103 | and returns the parsed datastructure." 104 | ([x] 105 | (spec {} x)) 106 | ([for-table x] 107 | (let [parsed 108 | (cond 109 | (string? x) (string-field x for-table) 110 | (vector? x) (field-spec x for-table) 111 | (map? x) (relationship x for-table) 112 | (list? x) (table x))] 113 | (assert parsed (str "Invalid spec: " x)) 114 | parsed))) 115 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 3 | Sqlium 0.1.3-SNAPSHOT

Sqlium 0.1.3-SNAPSHOT

Released under the Eclipse Public License

A flexible, Datomic-inspired, config-driven Extraction system (big E in ETL).

Installation

To install, add the following dependency to your project or build file:

[org.purefn/sqlium "0.1.3-SNAPSHOT"]

Namespaces

org.purefn.sqlium

A flexible, config-driven, Datomic-inspired entity extraction
4 | system. sqlium provides a declarative DSL to describe entities
5 | stored in a relational database - particularly those that are
6 | composed of many tables with nested relationships - and mechanisms
7 | to read and transform them into Clojure datastructures.

Public variables and functions:

-------------------------------------------------------------------------------- /docs/org.purefn.sqlium.html: -------------------------------------------------------------------------------- 1 | 3 | org.purefn.sqlium documentation

org.purefn.sqlium

A flexible, config-driven, Datomic-inspired entity extraction
 4 | system. sqlium provides a declarative DSL to describe entities
 5 | stored in a relational database - particularly those that are
 6 | composed of many tables with nested relationships - and mechanisms
 7 | to read and transform them into Clojure datastructures.

entity

(entity db spec id)
Returns a single entity by id for given spec, querying from jdbc
 8 | datasource db.

record-id

(record-id record)
Returns the id of a record.
 9 | 

records

(records db spec & options)
Returns a lazy sequence of records for spec, querying from jdbc
10 | datasource db. Takes optional parameters as kwargs or a map to
11 | either control expiry, return updated data since a given date by
12 | comparing against provided date time fields, or return updated data
13 | based on a specific update table. Only the highest-precedence
14 | option present will be used. In order of precedence:
15 | 
16 | :update - a map with:
17 |   * :table    string name of the update table
18 |   * :id       string name of the column containing entity ids to update
19 |   * :updated  string name of the field containing the entity update time
20 |   * :date     anything that can be coerced to a DateTime; the records
21 |               returned will be newer than this date
22 | 
23 | :delta - a map with:
24 |   * :fields   collection of :table/column datetime fields which will
25 |               be compared with :date to detect updated data
26 |   * :date     anything that can be coerced to a DateTime; the records
27 |               returned will be newer than this date
28 | 
29 | :expiry - a map with:
30 |   * :field    :table/column keyword for the datetime field that
31 |               determines the age of the entity
32 |   * :age      maximum age before the entity is ignored, either as an
33 |               integer number of days or an expiration date as something
34 |               that can be coerced to a DateTime
-------------------------------------------------------------------------------- /src/org/purefn/sqlium/transform.clj: -------------------------------------------------------------------------------- 1 | (ns org.purefn.sqlium.transform 2 | "Functions that deal with transforming data returned from the 3 | service's SQL queries into the structure defined by the data spec." 4 | (:require [clojure.string :as str] 5 | [clojure.set :as set])) 6 | 7 | (defn binary-string 8 | "Takes a byte array, returns a UTF-encoded string." 9 | [byte-array] 10 | (when byte-array 11 | (String. byte-array "UTF-8"))) 12 | 13 | (defn xform-fn 14 | "Takes the xform field from a field spec and returns a transform 15 | function. Handles mapping of built-in transforms." 16 | [xform] 17 | (binding [*ns* (the-ns 'org.purefn.sqlium.transform)] 18 | (eval xform))) 19 | 20 | (defn- alias-path 21 | "Transforms an alias into either a keyword, or path vector of 22 | keywords if it contains a path." 23 | [alias] 24 | (let [path (map keyword (str/split alias #"\."))] 25 | (if (> (count path) 1) 26 | (vec path) 27 | (first path)))) 28 | 29 | (defn field-transform 30 | "Takes a table spec and field spec, returns a function that takes a record and 31 | returns a keyval for the transformed field." 32 | [table field] 33 | (let [table-name (:name table) 34 | {:keys [alias column transform]} field 35 | field-key (if alias 36 | (alias-path alias) 37 | column) 38 | source (keyword (name table-name) 39 | (name column)) 40 | xform (if transform 41 | (xform-fn transform) 42 | identity)] 43 | (fn [record] 44 | [field-key (xform (get record source))]))) 45 | 46 | (defn table-fields-transform 47 | "Takes a table spec, returns a function that takes a record and 48 | returns a map with the table data formatted according to the table 49 | spec." 50 | [table-spec] 51 | (let [record-fields (if (seq (:fields table-spec)) 52 | (->> (:fields table-spec) 53 | (map (partial field-transform table-spec)) 54 | (apply juxt)) 55 | (constantly nil))] 56 | (fn [record] 57 | (reduce (fn [m [k-path v]] 58 | (cond (nil? v) 59 | m 60 | (vector? k-path) 61 | (assoc-in m k-path v) 62 | :else 63 | (assoc m k-path v))) 64 | {} 65 | (record-fields record))))) 66 | 67 | (defn single-relationship-transform 68 | "Takes a spec for a single relationship, and returns a function of 69 | two args: the record to be transformed for the relationship, and 70 | the already-transformed data map, that returns the updated data map 71 | with the relationship data added according to the relationship 72 | spec." 73 | [one-rel] 74 | (let [{:keys [target path]} one-rel 75 | table-xform (table-fields-transform target) 76 | merge-fn (if (seq path) 77 | (fn [data table-data] 78 | (update-in data (vec path) merge table-data)) 79 | (fn [data table-data] 80 | (merge table-data data)))] 81 | (fn [record data] 82 | (let [table-data (table-xform record)] 83 | (merge-fn data table-data))))) 84 | 85 | (declare group-transform) 86 | 87 | ;; TODO: in analysis phase, make sure to re-root the path for a many 88 | ;; relationship's group 89 | 90 | (defn many-relationship-transform 91 | "Takes a spec for a many relationship, and returns a function that 92 | takes a record and transformed data, and returns the transformed 93 | data with the many relationship data added according to the 94 | relationship spec." 95 | ;; Here be recursion 96 | [many-rel] 97 | (let [{:keys [alias column target path]} many-rel 98 | xform (group-transform target) 99 | source-field [:many-relationships column]] 100 | (fn [record data] 101 | (let [xformed (map xform (get-in record source-field))] 102 | (assoc-in data (vec path) xformed))))) 103 | 104 | (defn group-transform* 105 | "Main API function. Takes a root table spec, returns a function that 106 | takes a record and transforms the data for the group - the root 107 | table and its relationships, recursively transforming many 108 | relationship data - according to the spec." 109 | [table-spec] 110 | (let [single-rels (get-in table-spec [:relationships :one]) 111 | many-rels (get-in table-spec [:relationships :many]) 112 | base-xform (table-fields-transform table-spec) 113 | single-transforms (map single-relationship-transform single-rels) 114 | many-transforms (map many-relationship-transform many-rels)] 115 | (fn [record] 116 | (reduce (fn [xformed-data xf] 117 | (xf record xformed-data)) 118 | (base-xform record) 119 | (concat single-transforms many-transforms))))) 120 | 121 | (def group-transform (memoize group-transform*)) 122 | -------------------------------------------------------------------------------- /src/org/purefn/sqlium/dsl/analyze.clj: -------------------------------------------------------------------------------- 1 | (ns org.purefn.sqlium.dsl.analyze) 2 | 3 | (defn one-to-one? 4 | "True if relationship for table is a one-to-many." 5 | [{:keys [source-table column] :as relationship}] 6 | (= (:name source-table) 7 | (namespace column))) 8 | 9 | (def one-to-many? 10 | (complement one-to-one?)) 11 | 12 | (defn relationship? 13 | "True if x is a relationship spec." 14 | [x] 15 | (= :relationship (:type x))) 16 | 17 | (defn field? 18 | "True if x is a field spec." 19 | [x] 20 | (= :field (:type x))) 21 | 22 | (defn relationship-type 23 | "Takes a table and relationship. Returns sither :one-to-one if a 24 | one-to-one relationship, or :one-to-many if a one-to-many 25 | relationship." 26 | [relationship] 27 | (assert (relationship? relationship)) 28 | (cond (one-to-many? relationship) :many 29 | (one-to-one? relationship) :one)) 30 | 31 | (defn group-fields 32 | "Takes collection of fields, groups by field :type." 33 | [fields] 34 | (group-by :type fields)) 35 | 36 | (defn by-type 37 | "Groups relationships by relationship type." 38 | [relationships] 39 | (group-by relationship-type relationships)) 40 | 41 | (defn add-path 42 | "Adds relationship's top-level path, using either the :as alias or 43 | column name as the segment." 44 | [relationship] 45 | (let [{:keys [flatten alias column]} relationship 46 | many? (one-to-many? relationship) 47 | path-seg (if flatten 48 | [] 49 | [(keyword (cond alias (name alias) 50 | many? (str "_" (name column)) 51 | :else (name column)))])] 52 | (assoc relationship :path path-seg))) 53 | 54 | (defn path-prepender 55 | "Takes a path segment and returns a function that takes a 56 | relationship and returns the relationship with the path segment 57 | prepended to its path." 58 | [path-seg] 59 | (let [prepend-path (partial concat path-seg)] 60 | (fn [rel] 61 | (update rel :path prepend-path)))) 62 | 63 | (defn- splice-rels-with-path 64 | "Takes a relationship type (either `:one` or `:many`) and a 65 | collection of relationships, and returns a tuple: a collection of 66 | the first level of nested relationships of the given type with the 67 | path segment from their parent relationship prepended to their 68 | path, and the input relationships with the nested relationships of 69 | that type removed." 70 | [type rels] 71 | (assert (#{:one :many} type)) 72 | (let [spliced (mapcat (fn [rel] 73 | (let [path-prepend (path-prepender (:path rel))] 74 | (->> (get-in rel [:target :relationships type]) 75 | (map path-prepend)))) 76 | rels) 77 | rels-without-spliced (map (fn [rel] (update-in rel [:target :relationships] dissoc type)) rels)] 78 | [spliced rels-without-spliced])) 79 | 80 | (defn promote-relationships 81 | "Takes a relationship map with `:one` and `:many` keys. Returns a 82 | new relationship map where the first level of nested `:one` 83 | relationships has been promoted to the top level, and the `:many` 84 | relationships of all the resulting single relationships have been 85 | promoted top the top level `:many` relationships. Promoted 86 | relationships have the path segment from their parent's path 87 | prepended to their own." 88 | [rels] 89 | (let [{:keys [one many]} rels 90 | one-with-promoted (apply concat (splice-rels-with-path :one one)) 91 | [promoted-many one-without-many] (splice-rels-with-path :many one-with-promoted)] 92 | (cond-> {:one one-without-many} 93 | (or (seq many) (seq promoted-many)) 94 | (assoc :many (concat many promoted-many))))) 95 | 96 | (defn repeated-join-tables 97 | "Takes a collection of relationships and returns a collection of any 98 | repeated tables, or nil if none." 99 | [rels] 100 | (some->> rels 101 | (group-by (comp :name :target)) 102 | (filter (fn [[table rels]] 103 | (and table (> (count rels) 1)))) 104 | (seq) 105 | (into {}))) 106 | 107 | ;; TODO: build some tests around complicated path scenarios, figure 108 | ;; out re-rooting for many paths. 109 | 110 | (defn group-relationships 111 | "Takes a parsed data spec. Breaks out relationships from a table's 112 | fields, groups them into one-to-one and one-to-many, promotes the 113 | one-to-ones up to the root level, and also promotes the first level 114 | of many-to-many to the root level. 115 | 116 | It also builds a path for each relationship that describes how to 117 | associate data into the correct shape for the spec. 118 | 119 | The effect is that all the groups of one-to-one relationships that 120 | can be queried from the database together are grouped at the same 121 | level together, and all of each group's one-to-many relationships 122 | are at the top level of the group. 123 | 124 | One-to-many relationships constitute deeper levels in the tree. 125 | 126 | Eg., a spec like this (non-essential parts elided): 127 | {:type :table 128 | :name \"foo\" 129 | :fields ({:type :relationship 130 | :source-table {:name \"foo\"} 131 | :column :foo/bar_id 132 | :target 133 | {:type :table 134 | :name \"bar\" 135 | :fields ({:type :relationship 136 | :source-table {:name \"bar\"} 137 | :column :bar/fizz_id 138 | :target 139 | {:type :table 140 | :name \"fizz\" 141 | :fields ({:type :relationship 142 | :source-table {:name \"fizz\"} 143 | :column :buzz/fizz_id 144 | :target 145 | {:type :table 146 | :name \"buzz\"}})}})}} 147 | {:type :relationship 148 | :source-table {:name \"foo\"} 149 | :column :baz/foo_id 150 | :target 151 | {:type :table 152 | :name \"baz\"}})} 153 | 154 | Would get analyzed to: 155 | {:type :table 156 | :name \"foo\" 157 | :relationships 158 | {:one [{:type :relationship 159 | :source-table {:name \"foo\"} 160 | :column :foo/bar_id 161 | :path (:bar_id) 162 | :target {:type :table 163 | :name \"bar\"}} 164 | {:type :relationship 165 | :source-table {:name \"bar\"} 166 | :column :bar/fizz_id 167 | :path (:bar_id :fizz_id) 168 | :target {:type :table 169 | :name \"fizz\"}}] 170 | :many [{:type :relationship 171 | :source-table {:name \"foo\"} 172 | :column :baz/foo_id 173 | :path 174 | :target {:type :table 175 | :name \"baz\"}} 176 | {:type :relationship 177 | :source-table {:name \"fizz\"} 178 | :column :buzz/fizz_id 179 | :target {:type :table 180 | :name \"buzz\"}}]}} " 181 | [table] 182 | (let [fields (filter field? (:fields table)) 183 | relationships 184 | (->> (:fields table) 185 | (filter relationship?) 186 | (map (fn [r] (update r :target group-relationships))) 187 | (map add-path) 188 | (by-type)) 189 | promoted-relationships (promote-relationships relationships) 190 | repeated (repeated-join-tables (:one promoted-relationships))] 191 | (when repeated 192 | (throw (ex-info "Invalid relationships detected - repeated use of the same table." 193 | {:repeated repeated 194 | :source-table (dissoc table :fields)}))) 195 | (assoc table 196 | :fields fields 197 | :relationships promoted-relationships))) 198 | -------------------------------------------------------------------------------- /src/org/purefn/sqlium/sql.clj: -------------------------------------------------------------------------------- 1 | (ns org.purefn.sqlium.sql 2 | (:require [clojure.string :as str] 3 | [clj-time.coerce :as tc] 4 | [clj-time.format :as tf] 5 | [org.purefn.sqlium.dsl :as dsl])) 6 | 7 | (def field-split-str "_sqlfield_") 8 | 9 | (defn- order-rels 10 | "Recursion for `dependency-ordered`. Consumers should generally only 11 | call the 2-arg version. available-tables should be a set of table 12 | names that are already in the relation being joined into." 13 | ([rels avail-tables] 14 | (order-rels (into clojure.lang.PersistentQueue/EMPTY rels) avail-tables [] 0)) 15 | ([rels avail-tables ordered iter-num] 16 | (if-let [rel (first rels)] 17 | (cond (> iter-num (count rels)) 18 | (throw (ex-info "Unable to order relationships - infinite loop hit." 19 | {:remaining rels 20 | :ordered ordered 21 | :tables-in-relation avail-tables 22 | :iterations-with-no-progress iter-num})) 23 | (contains? avail-tables (get-in rel [:source-table :name])) 24 | (order-rels (pop rels) (conj avail-tables (get-in rel [:target :name])) 25 | (conj ordered rel) 0) 26 | :else 27 | (order-rels (-> rels pop (conj rel)) avail-tables ordered (inc iter-num))) 28 | ordered))) 29 | 30 | (defn dependency-ordered 31 | "Takes a collection of relationships and the root table, and returns 32 | the relationships in a dependency-satisfied order." 33 | [root-table rels] 34 | (order-rels rels #{(:name root-table)})) 35 | 36 | (defn column-name 37 | "Returns the column name from a keyword." 38 | [kw] 39 | (let [table (namespace kw) 40 | column (name kw)] 41 | (str table (when table ".") column))) 42 | 43 | (defn prefixed-column 44 | "Returns a prefixed column from a table name and column name, for 45 | use in SQL statements." 46 | [table column] 47 | (str table "." column)) 48 | 49 | (defn alias-column 50 | "Returns a column alias from a table and column name, for 51 | deterministic control of query results." 52 | [table-name column] 53 | (keyword (str table-name field-split-str column))) 54 | 55 | (defn field-column 56 | "Returns column name from a field as string." 57 | [field] 58 | (name (:column field))) 59 | 60 | (defn- left-join-statement 61 | "Builds LEFT JOIN statement, joining target on lfield to rfield." 62 | [target lfield rfield] 63 | (format "LEFT JOIN %s ON %s = %s" 64 | target lfield rfield)) 65 | 66 | (defn- single-join 67 | "Returns join in the form returned from table-join for a single 68 | rel." 69 | [{:keys [column target] :as rel}] 70 | [(:name target) 71 | (column-name column) 72 | (prefixed-column (:name target) (:id target))]) 73 | 74 | (defn- many-join 75 | "Returns join in the form returned from table-join for a many rel." 76 | [{:keys [column source-table target] :as rel}] 77 | [(:name target) 78 | (prefixed-column (:name source-table) (:id source-table)) 79 | (column-name column)]) 80 | 81 | (defn- table-joins 82 | "Returns collection of joins for a table, optionally recursively 83 | calling itself for many relationships. Joins returned are tuple 84 | vectors in the form: 85 | 86 | [target-table from-field to-field]" 87 | ([table-spec] 88 | (table-joins table-spec false)) 89 | ([table-spec recurse?] 90 | (let [{:keys [one many]} (:relationships table-spec)] 91 | (when (or (seq one) (seq many)) 92 | (let [single-joins (->> one 93 | (dependency-ordered table-spec) 94 | (map single-join)) 95 | many-joins (when recurse? 96 | (map many-join many))] 97 | (concat single-joins 98 | (when (and (seq many) recurse?) 99 | (concat (map many-join many) 100 | (mapcat (fn [many-rel] 101 | (table-joins (:target many-rel) recurse?)) 102 | many))))))))) 103 | 104 | (defn- many-condition 105 | "Returns the condition clause for a many relationship." 106 | [rel inputs] 107 | (let [rel-col (:column rel)] 108 | (format "%s IN (%s)" 109 | (column-name rel-col) 110 | (str/join ", " inputs)))) 111 | 112 | ;;; Main API functions 113 | 114 | (defn aliased-fields-statement 115 | "Takes a map of `:aliased-query-column-name` to `:table/column` and 116 | returns a SQL fields statement, eg \"SELECT FROM ...\"" 117 | [alias-map] 118 | (->> alias-map 119 | (map (fn [[alias col]] 120 | (str (column-name col) 121 | " AS " (name alias)))) 122 | (str/join ", "))) 123 | 124 | (defn from-statement 125 | "Builds a from statement from table spec. Optionally, recurses 126 | through the many relationships and adds them to the statement." 127 | ([table] 128 | (from-statement table false)) 129 | ([table recurse?] 130 | (let [table-name (:name table) 131 | joins (table-joins table recurse?)] 132 | (str "FROM " table-name " " 133 | (->> joins 134 | (map (partial apply left-join-statement)) 135 | (str/join " ")))))) 136 | 137 | (defn column-mappings 138 | "Returns a map of `:aliased-query-column-name` to `:table/column` 139 | from a table spec." 140 | [table] 141 | (let [{:keys [name id fields]} table 142 | columns (-> (map field-column fields) 143 | set 144 | (conj id))] 145 | (->> columns 146 | (map (juxt (partial alias-column name) (partial keyword name))) 147 | (into {})))) 148 | 149 | (defn group-column-mappings 150 | "Returns a map of `:aliased-query-column` to `:table/column` for all 151 | the columns in a table spec's query group (itself and its single 152 | relationships.)" 153 | [table] 154 | (->> (dsl/group-tables table) 155 | (map column-mappings) 156 | (reduce merge))) 157 | 158 | (defn mysql-date-string 159 | "Takes a date coerceable to LocalDateTime, and returns mysql-formatted 160 | date time string." 161 | [date] 162 | (tf/unparse-local (tf/formatters :mysql) 163 | (tc/to-local-date-time date))) 164 | 165 | (defn condition-sql 166 | "Takes a condition map, and optional table alias for the column's 167 | table, and returns a SQL condition string fragment. Default 168 | comparator is \"=\". 169 | 170 | Eg, `{:column :foo/bar 171 | :comparator \">\" 172 | :value 5}` 173 | 174 | Becomes \"foo.bar > 5\"" 175 | ([condition] 176 | (condition-sql condition nil)) 177 | ([condition table-alias] 178 | (let [{:keys [column comparator value] 179 | :or {comparator "="}} condition] 180 | (str (or table-alias (namespace column)) 181 | "." (name column) 182 | " " comparator " " 183 | (cond 184 | (string? value) (str "'" value "'") 185 | (nil? value) (str "NULL") 186 | :default value))))) 187 | 188 | (defn inner-join-sql 189 | "Takes a join map and returns SQL fragment for an inner join. 190 | Join map has :base and :target keys, each of which are maps of: 191 | 192 | * :name table name string 193 | * :alias optional table alias string 194 | * :join-col string name of the join column" 195 | [join] 196 | (let [{:keys [base target]} join] 197 | (str "INNER JOIN " (:name target) " AS " (:alias target) 198 | " ON " 199 | (or (:alias base) (:name base)) "." (:join-col base) 200 | " = " 201 | (or (:alias target) (:name target)) "." (:join-col target)))) 202 | 203 | (defn with-limit 204 | "Adds a limit and optionally an offset to query string." 205 | ([query limit] 206 | (with-limit query limit nil)) 207 | ([query limit offset] 208 | (str query " LIMIT " limit 209 | (when offset (str " OFFSET " offset))))) 210 | 211 | (defn in-statement 212 | "Returns an IN statement that can be used in a WHERE clause to match 213 | a column against a set of values." 214 | [{:keys [field vals] :as in}] 215 | (let [formatted-vals (map (fn [x] (if (string? x) 216 | (str \' x \') 217 | x)) 218 | vals)] 219 | (str (column-name field) " IN ( " 220 | (str/join ", " formatted-vals) " )"))) 221 | 222 | (defn select 223 | "Builds select statement from table. Optionally, brings in levels of 224 | many relationships in the from statement to use for filtering. 225 | Returns tuple of [sql-query column-alias-map]." 226 | [table] 227 | (let [cols-map (group-column-mappings table)] 228 | [(str "SELECT " (aliased-fields-statement cols-map) 229 | " " (from-statement table)) cols-map])) 230 | 231 | (defn many-relationship-select 232 | "Takes a many relationship and data returned from the parent query 233 | and builds a query to select the many relationship data. Returns tuple of [sql-query column-alias-map]." 234 | [rel data] 235 | (let [source-col (dsl/id-column (:source-table rel)) 236 | rel-column (:column rel) 237 | table (update (:target rel) :fields 238 | conj {:type :field :column rel-column}) 239 | inputs (keep source-col data) 240 | [select-str cols-map] (select table)] 241 | (when (seq inputs) 242 | [(str select-str " WHERE " (many-condition rel inputs)) cols-map]))) 243 | -------------------------------------------------------------------------------- /docs/js/highlight.min.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.6.0 | BSD3 License | git.io/hljslicense */ 2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/[&<>]/gm,function(e){return I[e]})}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return R(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||R(i))return i}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){l+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):E(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"===e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function g(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function h(e,n,t,r){var a=r?"":y.classPrefix,i='',i+n+o}function p(){var e,t,r,a;if(!E.k)return n(B);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(B);r;)a+=n(B.substr(t,r.index-t)),e=g(E,r),e?(M+=e[1],a+=h(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(B);return a+n(B.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!x[E.sL])return n(B);var t=e?l(E.sL,B,!0,L[E.sL]):f(B,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(L[E.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){k+=null!=E.sL?d():p(),B=""}function v(e){k+=e.cN?h(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(B+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?B+=n:(t.eB&&(B+=n),b(),t.rB||t.eB||(B=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?B+=n:(a.rE||a.eE||(B+=n),b(),a.eE&&(B=n));do E.cN&&(k+=C),E.skip||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return B+=n,n.length||1}var N=R(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var w,E=i||N,L={},k="";for(w=E;w!==N;w=w.parent)w.cN&&(k=h(w.cN,"",!0)+k);var B="",M=0;try{for(var I,j,O=0;;){if(E.t.lastIndex=O,I=E.t.exec(t),!I)break;j=m(t.substr(O,I.index-O),I[0]),O=I.index+j}for(m(t.substr(O)),w=E;w.parent;w=w.parent)w.cN&&(k+=C);return{r:M,value:k,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function f(e,t){t=t||y.languages||E(x);var r={r:0,value:n(e)},a=r;return t.filter(R).forEach(function(n){var t=l(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function g(e){return y.tabReplace||y.useBR?e.replace(M,function(e,n){return y.useBR&&"\n"===e?"
":y.tabReplace?n.replace(/\t/g,y.tabReplace):void 0}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function p(e){var n,t,r,o,s,p=i(e);a(p)||(y.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,s=n.textContent,r=p?l(p,s,!0):f(s),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),s)),r.value=g(r.value),e.innerHTML=r.value,e.className=h(e.className,p,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function d(e){y=o(y,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");w.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function N(){return E(x)}function R(e){return e=(e||"").toLowerCase(),x[e]||x[L[e]]}var w=[],E=Object.keys,x={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",y={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},I={"&":"&","<":"<",">":">"};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=R,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("clojure",function(e){var t={"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},r="a-zA-Z_\\-!.?+*=<>&#'",n="["+r+"]["+r+"0-9/;:]*",a="[-+]?\\d+(\\.\\d+)?",o={b:n,r:0},s={cN:"number",b:a,r:0},i=e.inherit(e.QSM,{i:null}),c=e.C(";","$",{r:0}),d={cN:"literal",b:/\b(true|false|nil)\b/},l={b:"[\\[\\{]",e:"[\\]\\}]"},m={cN:"comment",b:"\\^"+n},p=e.C("\\^\\{","\\}"),u={cN:"symbol",b:"[:]{1,2}"+n},f={b:"\\(",e:"\\)"},h={eW:!0,r:0},y={k:t,l:n,cN:"name",b:n,starts:h},b=[f,i,m,p,c,u,l,s,d,o];return f.c=[e.C("comment",""),y,h],h.c=b,l.c=b,{aliases:["clj"],i:/\S/,c:[f,i,m,p,c,u,l,s,d]}});hljs.registerLanguage("clojure-repl",function(e){return{c:[{cN:"meta",b:/^([\w.-]+|\s*#_)=>/,starts:{e:/$/,sL:"clojure"}}]}}); -------------------------------------------------------------------------------- /docs/css/default.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial, sans-serif; 3 | font-size: 15px; 4 | } 5 | 6 | pre, code { 7 | font-family: Monaco, DejaVu Sans Mono, Consolas, monospace; 8 | font-size: 9pt; 9 | margin: 15px 0; 10 | } 11 | 12 | h1 { 13 | font-weight: normal; 14 | font-size: 29px; 15 | margin: 10px 0 2px 0; 16 | padding: 0; 17 | } 18 | 19 | h2 { 20 | font-weight: normal; 21 | font-size: 25px; 22 | } 23 | 24 | h5.license { 25 | margin: 9px 0 22px 0; 26 | color: #555; 27 | font-weight: normal; 28 | font-size: 12px; 29 | font-style: italic; 30 | } 31 | 32 | .document h1, .namespace-index h1 { 33 | font-size: 32px; 34 | margin-top: 12px; 35 | } 36 | 37 | #header, #content, .sidebar { 38 | position: fixed; 39 | } 40 | 41 | #header { 42 | top: 0; 43 | left: 0; 44 | right: 0; 45 | height: 22px; 46 | color: #f5f5f5; 47 | padding: 5px 7px; 48 | } 49 | 50 | #content { 51 | top: 32px; 52 | right: 0; 53 | bottom: 0; 54 | overflow: auto; 55 | background: #fff; 56 | color: #333; 57 | padding: 0 18px; 58 | } 59 | 60 | .sidebar { 61 | position: fixed; 62 | top: 32px; 63 | bottom: 0; 64 | overflow: auto; 65 | } 66 | 67 | .sidebar.primary { 68 | background: #e2e2e2; 69 | border-right: solid 1px #cccccc; 70 | left: 0; 71 | width: 250px; 72 | } 73 | 74 | .sidebar.secondary { 75 | background: #f2f2f2; 76 | border-right: solid 1px #d7d7d7; 77 | left: 251px; 78 | width: 200px; 79 | } 80 | 81 | #content.namespace-index, #content.document { 82 | left: 251px; 83 | } 84 | 85 | #content.namespace-docs { 86 | left: 452px; 87 | } 88 | 89 | #content.document { 90 | padding-bottom: 10%; 91 | } 92 | 93 | #header { 94 | background: #3f3f3f; 95 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); 96 | z-index: 100; 97 | } 98 | 99 | #header h1 { 100 | margin: 0; 101 | padding: 0; 102 | font-size: 18px; 103 | font-weight: lighter; 104 | text-shadow: -1px -1px 0px #333; 105 | } 106 | 107 | #header h1 .project-version { 108 | font-weight: normal; 109 | } 110 | 111 | .project-version { 112 | padding-left: 0.15em; 113 | } 114 | 115 | #header a, .sidebar a { 116 | display: block; 117 | text-decoration: none; 118 | } 119 | 120 | #header a { 121 | color: #f5f5f5; 122 | } 123 | 124 | .sidebar a { 125 | color: #333; 126 | } 127 | 128 | #header h2 { 129 | float: right; 130 | font-size: 9pt; 131 | font-weight: normal; 132 | margin: 4px 3px; 133 | padding: 0; 134 | color: #bbb; 135 | } 136 | 137 | #header h2 a { 138 | display: inline; 139 | } 140 | 141 | .sidebar h3 { 142 | margin: 0; 143 | padding: 10px 13px 0 13px; 144 | font-size: 19px; 145 | font-weight: lighter; 146 | } 147 | 148 | .sidebar h3 a { 149 | color: #444; 150 | } 151 | 152 | .sidebar h3.no-link { 153 | color: #636363; 154 | } 155 | 156 | .sidebar ul { 157 | padding: 7px 0 6px 0; 158 | margin: 0; 159 | } 160 | 161 | .sidebar ul.index-link { 162 | padding-bottom: 4px; 163 | } 164 | 165 | .sidebar li { 166 | display: block; 167 | vertical-align: middle; 168 | } 169 | 170 | .sidebar li a, .sidebar li .no-link { 171 | border-left: 3px solid transparent; 172 | padding: 0 10px; 173 | white-space: nowrap; 174 | } 175 | 176 | .sidebar li .no-link { 177 | display: block; 178 | color: #777; 179 | font-style: italic; 180 | } 181 | 182 | .sidebar li .inner { 183 | display: inline-block; 184 | padding-top: 7px; 185 | height: 24px; 186 | } 187 | 188 | .sidebar li a, .sidebar li .tree { 189 | height: 31px; 190 | } 191 | 192 | .depth-1 .inner { padding-left: 2px; } 193 | .depth-2 .inner { padding-left: 6px; } 194 | .depth-3 .inner { padding-left: 20px; } 195 | .depth-4 .inner { padding-left: 34px; } 196 | .depth-5 .inner { padding-left: 48px; } 197 | .depth-6 .inner { padding-left: 62px; } 198 | 199 | .sidebar li .tree { 200 | display: block; 201 | float: left; 202 | position: relative; 203 | top: -10px; 204 | margin: 0 4px 0 0; 205 | padding: 0; 206 | } 207 | 208 | .sidebar li.depth-1 .tree { 209 | display: none; 210 | } 211 | 212 | .sidebar li .tree .top, .sidebar li .tree .bottom { 213 | display: block; 214 | margin: 0; 215 | padding: 0; 216 | width: 7px; 217 | } 218 | 219 | .sidebar li .tree .top { 220 | border-left: 1px solid #aaa; 221 | border-bottom: 1px solid #aaa; 222 | height: 19px; 223 | } 224 | 225 | .sidebar li .tree .bottom { 226 | height: 22px; 227 | } 228 | 229 | .sidebar li.branch .tree .bottom { 230 | border-left: 1px solid #aaa; 231 | } 232 | 233 | .sidebar.primary li.current a { 234 | border-left: 3px solid #a33; 235 | color: #a33; 236 | } 237 | 238 | .sidebar.secondary li.current a { 239 | border-left: 3px solid #33a; 240 | color: #33a; 241 | } 242 | 243 | .namespace-index h2 { 244 | margin: 30px 0 0 0; 245 | } 246 | 247 | .namespace-index h3 { 248 | font-size: 16px; 249 | font-weight: bold; 250 | margin-bottom: 0; 251 | } 252 | 253 | .namespace-index .topics { 254 | padding-left: 30px; 255 | margin: 11px 0 0 0; 256 | } 257 | 258 | .namespace-index .topics li { 259 | padding: 5px 0; 260 | } 261 | 262 | .namespace-docs h3 { 263 | font-size: 18px; 264 | font-weight: bold; 265 | } 266 | 267 | .public h3 { 268 | margin: 0; 269 | float: left; 270 | } 271 | 272 | .usage { 273 | clear: both; 274 | } 275 | 276 | .public { 277 | margin: 0; 278 | border-top: 1px solid #e0e0e0; 279 | padding-top: 14px; 280 | padding-bottom: 6px; 281 | } 282 | 283 | .public:last-child { 284 | margin-bottom: 20%; 285 | } 286 | 287 | .members .public:last-child { 288 | margin-bottom: 0; 289 | } 290 | 291 | .members { 292 | margin: 15px 0; 293 | } 294 | 295 | .members h4 { 296 | color: #555; 297 | font-weight: normal; 298 | font-variant: small-caps; 299 | margin: 0 0 5px 0; 300 | } 301 | 302 | .members .inner { 303 | padding-top: 5px; 304 | padding-left: 12px; 305 | margin-top: 2px; 306 | margin-left: 7px; 307 | border-left: 1px solid #bbb; 308 | } 309 | 310 | #content .members .inner h3 { 311 | font-size: 12pt; 312 | } 313 | 314 | .members .public { 315 | border-top: none; 316 | margin-top: 0; 317 | padding-top: 6px; 318 | padding-bottom: 0; 319 | } 320 | 321 | .members .public:first-child { 322 | padding-top: 0; 323 | } 324 | 325 | h4.type, 326 | h4.dynamic, 327 | h4.added, 328 | h4.deprecated { 329 | float: left; 330 | margin: 3px 10px 15px 0; 331 | font-size: 15px; 332 | font-weight: bold; 333 | font-variant: small-caps; 334 | } 335 | 336 | .public h4.type, 337 | .public h4.dynamic, 338 | .public h4.added, 339 | .public h4.deprecated { 340 | font-size: 13px; 341 | font-weight: bold; 342 | margin: 3px 0 0 10px; 343 | } 344 | 345 | .members h4.type, 346 | .members h4.added, 347 | .members h4.deprecated { 348 | margin-top: 1px; 349 | } 350 | 351 | h4.type { 352 | color: #717171; 353 | } 354 | 355 | h4.dynamic { 356 | color: #9933aa; 357 | } 358 | 359 | h4.added { 360 | color: #508820; 361 | } 362 | 363 | h4.deprecated { 364 | color: #880000; 365 | } 366 | 367 | .namespace { 368 | margin-bottom: 30px; 369 | } 370 | 371 | .namespace:last-child { 372 | margin-bottom: 10%; 373 | } 374 | 375 | .index { 376 | padding: 0; 377 | font-size: 80%; 378 | margin: 15px 0; 379 | line-height: 16px; 380 | } 381 | 382 | .index * { 383 | display: inline; 384 | } 385 | 386 | .index p { 387 | padding-right: 3px; 388 | } 389 | 390 | .index li { 391 | padding-right: 5px; 392 | } 393 | 394 | .index ul { 395 | padding-left: 0; 396 | } 397 | 398 | .type-sig { 399 | clear: both; 400 | color: #088; 401 | } 402 | 403 | .type-sig pre { 404 | padding-top: 10px; 405 | margin: 0; 406 | } 407 | 408 | .usage code { 409 | display: block; 410 | color: #008; 411 | margin: 2px 0; 412 | } 413 | 414 | .usage code:first-child { 415 | padding-top: 10px; 416 | } 417 | 418 | p { 419 | margin: 15px 0; 420 | } 421 | 422 | .public p:first-child, .public pre.plaintext { 423 | margin-top: 12px; 424 | } 425 | 426 | .doc { 427 | margin: 0 0 26px 0; 428 | clear: both; 429 | } 430 | 431 | .public .doc { 432 | margin: 0; 433 | } 434 | 435 | .namespace-index .doc { 436 | margin-bottom: 20px; 437 | } 438 | 439 | .namespace-index .namespace .doc { 440 | margin-bottom: 10px; 441 | } 442 | 443 | .markdown p, .markdown li, .markdown dt, .markdown dd, .markdown td { 444 | line-height: 22px; 445 | } 446 | 447 | .markdown li { 448 | padding: 2px 0; 449 | } 450 | 451 | .markdown h2 { 452 | font-weight: normal; 453 | font-size: 25px; 454 | margin: 30px 0 10px 0; 455 | } 456 | 457 | .markdown h3 { 458 | font-weight: normal; 459 | font-size: 20px; 460 | margin: 30px 0 0 0; 461 | } 462 | 463 | .markdown h4 { 464 | font-size: 15px; 465 | margin: 22px 0 -4px 0; 466 | } 467 | 468 | .doc, .public, .namespace .index { 469 | max-width: 680px; 470 | overflow-x: visible; 471 | } 472 | 473 | .markdown pre > code { 474 | display: block; 475 | padding: 10px; 476 | } 477 | 478 | .markdown pre > code, .src-link a { 479 | border: 1px solid #e4e4e4; 480 | border-radius: 2px; 481 | } 482 | 483 | .markdown code:not(.hljs), .src-link a { 484 | background: #f6f6f6; 485 | } 486 | 487 | pre.deps { 488 | display: inline-block; 489 | margin: 0 10px; 490 | border: 1px solid #e4e4e4; 491 | border-radius: 2px; 492 | padding: 10px; 493 | background-color: #f6f6f6; 494 | } 495 | 496 | .markdown hr { 497 | border-style: solid; 498 | border-top: none; 499 | color: #ccc; 500 | } 501 | 502 | .doc ul, .doc ol { 503 | padding-left: 30px; 504 | } 505 | 506 | .doc table { 507 | border-collapse: collapse; 508 | margin: 0 10px; 509 | } 510 | 511 | .doc table td, .doc table th { 512 | border: 1px solid #dddddd; 513 | padding: 4px 6px; 514 | } 515 | 516 | .doc table th { 517 | background: #f2f2f2; 518 | } 519 | 520 | .doc dl { 521 | margin: 0 10px 20px 10px; 522 | } 523 | 524 | .doc dl dt { 525 | font-weight: bold; 526 | margin: 0; 527 | padding: 3px 0; 528 | border-bottom: 1px solid #ddd; 529 | } 530 | 531 | .doc dl dd { 532 | padding: 5px 0; 533 | margin: 0 0 5px 10px; 534 | } 535 | 536 | .doc abbr { 537 | border-bottom: 1px dotted #333; 538 | font-variant: none; 539 | cursor: help; 540 | } 541 | 542 | .src-link { 543 | margin-bottom: 15px; 544 | } 545 | 546 | .src-link a { 547 | font-size: 70%; 548 | padding: 1px 4px; 549 | text-decoration: none; 550 | color: #5555bb; 551 | } 552 | -------------------------------------------------------------------------------- /docs/org.purefn.sqlium.import.html: -------------------------------------------------------------------------------- 1 | 3 | org.purefn.sqlium.import documentation

org.purefn.sqlium.import

all-ids-query

(all-ids-query spec)
Returns a query that will select all ids in the base table of
 4 | spec.

condition-table

(condition-table condition)
Returns the table used in a condition's column.
 5 | 

default-batch-size

delta-condition

(delta-condition field date)
Returns condition map for a delta field.
 6 | 

expiry-condition

(expiry-condition expiry)
Returns condition map for an expiry.
 7 | 

fetch-column

(fetch-column db query column)
Efficient, low-level query function to return a collection of a
 8 | single column. Takes a sql query, the column name to fetch, and
 9 | returns an ArrayList with the values of that column.

fetch-results

(fetch-results db sql-with-aliases)
Takes the [sql col-aliases] tuple as returned by `sql/select` and
10 | fetches the results, renaming the keys in the returned records
11 | using col-aliases.

id-query

(id-query spec {:keys [limit offset delta expiry], :as opts})
Takes parsed spec (not grouped) and returns query to retrieve the
12 | ids, controlled by opts map.

import-many-relationship

(import-many-relationship db rel data)
Takes a db, a many relationship map and collection of source table data,
13 | retrieves the related data and merges it into the table data.

import-record

(import-record db table-spec entid)
Like import-table, but returns single record from given db.
14 | 

import-table

(import-table db table-spec)(import-table db table-spec {:keys [limit offset delta expiry batch update-table], :or {batch default-batch-size}, :as opts})
Takes an analyzed DSL spec with a table at the top level, and opts
15 | map, and performs the queries to fetch the table data and its
16 | associated relationships. opts map has keys:
17 | 
18 |  * :limit   Number of records to retrieve [unlimited]
19 |  * :offset  Starting record [0]
20 |  * :delta   Turns the import into a deltas import. A map with
21 |             parameters for retrieving deltas.
22 |  * :update-table  Also turns into ino a deltas import. Preferred
23 |             over :delta, if present. A map describing the table
24 |             that contains entity ids to update.
25 |  * :expiry  Spec for filter for records older than a specified
26 |             number of days.
27 |  * :batch   Batch size.
28 | 
29 | If present, the delta map should have keys:
30 |  * :fields  Collection of :table/column (datetime) fields that will
31 |             be used for update detection.
32 |  * :date    Date to be used for comparison. Anything that clj-time can
33 |             coerce to DateTime.
34 | 
35 | If present, the update-table map should have keys:
36 |  * :table     Name of the update table
37 |  * :id        Name of column with entity ids
38 |  * :updated   Name of column with entity update datetimes.
39 |  * :since     Datetime to return updates since.
40 | 
41 | If present, the expiry map should have keys:
42 |  * :field   The :table/column (datetime) field that will be used to
43 |             determine record age.
44 |  * :age     DateTime of the cutoff.
45 | 
46 | The resulting records have keys in the form of :table/column for
47 | each group, and many-relationships
48 | in [:many-relationships :foreign-table/column] keys.
49 | 
50 | Returns a lazy-seq of results.

join-path

(join-path path)
Takes a path (as from table-paths) and returns collection of maps
51 | describing each join in the path, containing:
52 | 
53 | `{:base {:name "table-name"
54 |          :alias "optional-alias"
55 |          :join-col "col-name"}
56 |   :target {:name "table-name"
57 |            :alias "table-alias"
58 |            :join-col "col-name"}`
59 | 
60 | Table aliases are used so that tables can be repeated in a
61 | relationship chain. The aliases are generated by appending an
62 | incrementing number to the table name.

join-results

(join-results rel data rel-data)
Takes a relationship map for a many relationship, table data, and
63 | the relationship's data, and joins the relationship data into the
64 | table data.

path-query

(path-query path conditions)
Takes a path as returned by `table-paths`, and a collection of
65 | conditions, and returns a query to retrieve ids from the base table
66 | in the path.
67 | 
68 | Each condition is a map of:
69 | 
70 | `{:column :table/col
71 |   :comparator "comparator-sym"
72 |   :value some-val}`

table-id

(table-id table)
Returns keyword representing a table id field, eg :table/field.
73 | 

table-paths

(table-paths table-spec)(table-paths paths base-path table-spec)
Returns a map, keyed by table name, with all paths to get to that
74 | table in given compiled spec.
75 | 
76 | A path is a vector of [table-with-rel* table-without-rel]
77 | 
78 | Returns  `{table-name #{path}}`

update-table-id-query

(update-table-id-query {:keys [table id updated since], :as update-map})
Takes a map describing update table, and returns a query to
79 | retrieve ids to update. Update map has keys:
80 | 
81 |   * :table     Name of the update table
82 |   * :id        Name of column with entity ids
83 |   * :updated   Name of column with entity update datetimes.
84 |   * :since     Datetime to return updates since.
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor to control, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /src/org/purefn/sqlium/import.clj: -------------------------------------------------------------------------------- 1 | (ns org.purefn.sqlium.import 2 | (:require [clojure.java.jdbc :as jdbc] 3 | [clojure.set :as set] 4 | [clojure.string :as str] 5 | [clojure.tools.logging :as log] 6 | [clj-time.core :as time] 7 | [clj-time.coerce :as tc] 8 | [org.purefn.sqlium.dsl :as dsl] 9 | [org.purefn.sqlium.sql :as sql]) 10 | (:import java.util.ArrayList 11 | java.sql.ResultSet)) 12 | 13 | (def default-batch-size 10000) 14 | 15 | (defn table-id 16 | "Returns keyword representing a table id field, eg :table/field." 17 | [table] 18 | (keyword (:name table) (:id table))) 19 | 20 | (defn join-results 21 | "Takes a relationship map for a many relationship, table data, and 22 | the relationship's data, and joins the relationship data into the 23 | table data." 24 | [rel data rel-data] 25 | (let [{:keys [source-table target column]} rel 26 | rel-data-lookup (group-by #(get % column) rel-data) 27 | id-col (dsl/id-column source-table)] 28 | (map (fn [rec] 29 | (let [rec-id (get rec id-col) 30 | rec-data (get rel-data-lookup rec-id)] 31 | (cond-> rec 32 | rec-data (assoc-in [:many-relationships column] rec-data)))) 33 | data))) 34 | 35 | (defn fetch-results 36 | "Takes the [sql col-aliases] tuple as returned by `sql/select` and 37 | fetches the results, renaming the keys in the returned records 38 | using col-aliases." 39 | [db sql-with-aliases] 40 | (let [[sql col-aliases] sql-with-aliases] 41 | ;; TODO: fix jdbc call 42 | (->> (jdbc/query db [sql]) 43 | (map #(set/rename-keys % col-aliases))))) 44 | 45 | (defn- result-set-column-list 46 | "Extracts a single column by name from each row of a ResultSet, 47 | adding it to an ArrayList." 48 | [^ResultSet rs colname] 49 | (let [alist (ArrayList. 1000)] 50 | (while (.next rs) 51 | (.add alist (.getObject rs colname))) 52 | alist)) 53 | 54 | (defn fetch-column 55 | "Efficient, low-level query function to return a collection of a 56 | single column. Takes a sql query, the column name to fetch, and 57 | returns an ArrayList with the values of that column." 58 | [db query column] 59 | (jdbc/db-query-with-resultset db [query] #(result-set-column-list % column))) 60 | 61 | (defn import-many-relationship 62 | "Takes a db, a many relationship map and collection of source table data, 63 | retrieves the related data and merges it into the table data." 64 | [db rel data] 65 | (when-let [sql-with-aliases (sql/many-relationship-select rel data)] 66 | (log/debug :fn "import-many-relationship" 67 | :query (first sql-with-aliases)) 68 | (let [many-data (fetch-results db sql-with-aliases) 69 | many-rels (get-in rel [:target :relationships :many])] 70 | (reduce (fn [data rel] 71 | (let [rel-data (import-many-relationship db rel data)] 72 | (join-results rel data rel-data))) 73 | many-data 74 | many-rels)))) 75 | 76 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 77 | ;; fetching ids 78 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 79 | 80 | (defn table-paths 81 | "Returns a map, keyed by table name, with all paths to get to that 82 | table in given compiled spec. 83 | 84 | A path is a vector of [table-with-rel* table-without-rel] 85 | 86 | Returns `{table-name #{path}}`" 87 | ([table-spec] 88 | (table-paths {} [] table-spec)) 89 | ([paths base-path table-spec] 90 | (let [{:keys [name fields]} table-spec 91 | rels (filter dsl/relationship? fields) 92 | base-table (dissoc table-spec :fields) 93 | table-path (conj base-path base-table) 94 | next-paths (update paths name (fnil conj #{}) table-path)] 95 | (reduce (fn [ps rel] 96 | (let [target (:target rel) 97 | base-rel (dissoc rel :target)] 98 | (table-paths ps (conj base-path 99 | (assoc base-table :relationship base-rel)) 100 | target))) 101 | next-paths 102 | rels)))) 103 | 104 | (defn join-path 105 | "Takes a path (as from table-paths) and returns collection of maps 106 | describing each join in the path, containing: 107 | 108 | `{:base {:name \"table-name\" 109 | :alias \"optional-alias\" 110 | :join-col \"col-name\"} 111 | :target {:name \"table-name\" 112 | :alias \"table-alias\" 113 | :join-col \"col-name\"}` 114 | 115 | Table aliases are used so that tables can be repeated in a 116 | relationship chain. The aliases are generated by appending an 117 | incrementing number to the table name." 118 | [path] 119 | (->> path 120 | (partition 2 1) 121 | (map-indexed 122 | (fn [idx [t1 t2]] 123 | (let [{t1-id :id t1-name :name} t1 124 | {t2-id :id t2-name :name} t2 125 | join-col (get-in t1 [:relationship :column]) 126 | many-rel? (when join-col 127 | (= t2-name (namespace join-col))) 128 | t1-join-col (if many-rel? 129 | t1-id 130 | (name join-col)) 131 | t2-join-col (if many-rel? 132 | (name join-col) 133 | t2-id) 134 | t1-alias (when (pos? idx) 135 | (str t1-name (dec idx))) 136 | t2-alias (str t2-name idx)] 137 | {:base {:name t1-name 138 | :alias t1-alias 139 | :join-col t1-join-col} 140 | :target {:name t2-name 141 | :alias t2-alias 142 | :join-col t2-join-col}}))))) 143 | 144 | (defn path-query 145 | "Takes a path as returned by `table-paths`, and a collection of 146 | conditions, and returns a query to retrieve ids from the base table 147 | in the path. 148 | 149 | Each condition is a map of: 150 | 151 | `{:column :table/col 152 | :comparator \"comparator-sym\" 153 | :value some-val}`" 154 | [path conditions] 155 | (let [{:keys [id name] :as base-table} (first path) 156 | base-query (format "SELECT %s AS id FROM %s" 157 | (str name "." id) 158 | name) 159 | joins? (> (count path) 1) 160 | jp (when joins? (join-path path)) 161 | target-table (if joins? 162 | (:target (last jp)) 163 | {:name name 164 | :alias name}) 165 | applicable-conds (filter #(= (:name target-table) 166 | (namespace (:column %))) 167 | conditions)] 168 | (str base-query 169 | (when joins? 170 | (str " " 171 | (->> jp 172 | (map sql/inner-join-sql) 173 | (str/join " ")))) 174 | (when (seq applicable-conds) 175 | (str " WHERE " 176 | (->> applicable-conds 177 | (map #(sql/condition-sql % (:alias target-table))) 178 | (str/join " OR "))))))) 179 | 180 | (defn expiry-condition 181 | "Returns condition map for an expiry." 182 | [expiry] 183 | (let [{:keys [field age]} expiry 184 | ;; picking an arbitrary cutoff number to distinguish # 185 | ;; of days from millisecond timestamp 186 | cutoff (if (and (number? age) (< age 100000)) 187 | (-> (time/now) 188 | (time/to-time-zone (time/default-time-zone)) 189 | (time/minus (time/days age)) 190 | (time/with-time-at-start-of-day)) 191 | age)] 192 | {:column field 193 | :comparator ">" 194 | :value (sql/mysql-date-string (tc/to-date-time cutoff))})) 195 | 196 | ;; TODO: push this time stuff to the client 197 | 198 | (defn delta-condition 199 | "Returns condition map for a delta field." 200 | [field date] 201 | {:column field 202 | :comparator ">" 203 | :value (sql/mysql-date-string (tc/to-date-time date))}) 204 | 205 | (defn condition-table 206 | "Returns the table used in a condition's column." 207 | [condition] 208 | (namespace (:column condition))) 209 | 210 | (defn all-ids-query 211 | "Returns a query that will select all ids in the base table of 212 | spec." 213 | [spec] 214 | (let [{:keys [id name]} spec] 215 | (format "SELECT %s AS id FROM %s" 216 | id name))) 217 | 218 | (defn update-table-id-query 219 | "Takes a map describing update table, and returns a query to 220 | retrieve ids to update. Update map has keys: 221 | 222 | * :table Name of the update table 223 | * :id Name of column with entity ids 224 | * :updated Name of column with entity update datetimes. 225 | * :since Datetime to return updates since." 226 | [{:keys [table id updated since] :as update-map}] 227 | (format "SELECT %s FROM %s WHERE %s" 228 | id table 229 | (sql/condition-sql {:column (keyword table updated) 230 | :comparator ">" 231 | :value (sql/mysql-date-string (tc/to-date-time since))}))) 232 | 233 | (defn id-query 234 | "Takes parsed spec (not grouped) and returns query to retrieve the 235 | ids, controlled by opts map." 236 | [spec {:keys [limit offset delta expiry] :as opts}] 237 | (let [tables (table-paths spec) 238 | conds (cond-> 239 | (when delta 240 | (map #(delta-condition % (:date delta)) 241 | (:fields delta))) 242 | expiry (conj (expiry-condition expiry))) 243 | cond-tables (set (map condition-table conds)) 244 | paths (mapcat (partial get tables) cond-tables) 245 | queries (map #(path-query % conds) paths)] 246 | (if (seq queries) 247 | (str/join " UNION " queries) 248 | (all-ids-query spec)))) 249 | 250 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 251 | ;; main API 252 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 253 | 254 | 255 | ;;; How this works 256 | 257 | ;; - queries for all the ids to fetch as part of the import 258 | ;; - this query uses all the conditions/options passed in 259 | ;; - joins in the full depth of the data spec, so we can have delta 260 | ;; and expiry fields anywhere in the entity 261 | ;; - returns a lazy seq, which will fetch the ids in batches of 262 | ;; batch option or `default-batch-size` 263 | 264 | ;; TODO: take db 265 | (defn import-table 266 | "Takes an analyzed DSL spec with a table at the top level, and opts 267 | map, and performs the queries to fetch the table data and its 268 | associated relationships. opts map has keys: 269 | 270 | * :limit Number of records to retrieve [unlimited] 271 | * :offset Starting record [0] 272 | * :delta Turns the import into a deltas import. A map with 273 | parameters for retrieving deltas. 274 | * :update-table Also turns into ino a deltas import. Preferred 275 | over :delta, if present. A map describing the table 276 | that contains entity ids to update. 277 | * :expiry Spec for filter for records older than a specified 278 | number of days. 279 | * :batch Batch size. 280 | 281 | If present, the delta map should have keys: 282 | * :fields Collection of :table/column (datetime) fields that will 283 | be used for update detection. 284 | * :date Date to be used for comparison. Anything that clj-time can 285 | coerce to DateTime. 286 | 287 | If present, the update-table map should have keys: 288 | * :table Name of the update table 289 | * :id Name of column with entity ids 290 | * :updated Name of column with entity update datetimes. 291 | * :since Datetime to return updates since. 292 | 293 | If present, the expiry map should have keys: 294 | * :field The :table/column (datetime) field that will be used to 295 | determine record age. 296 | * :age DateTime of the cutoff. 297 | 298 | The resulting records have keys in the form of :table/column for 299 | each group, and many-relationships 300 | in [:many-relationships :foreign-table/column] keys. 301 | 302 | Returns a lazy-seq of results." 303 | ([db table-spec] 304 | (import-table db table-spec {})) 305 | ([db table-spec {:keys [limit offset delta expiry batch update-table] 306 | :or {batch default-batch-size} :as opts}] 307 | (let [{:keys [grouped spec]} table-spec 308 | col-aliases (sql/group-column-mappings grouped) 309 | id-col (table-id grouped) 310 | ids-q (if update-table 311 | (update-table-id-query update-table) 312 | (id-query spec opts)) 313 | _ (log/debug :fn "import-table" 314 | :msg "Fetching ids" 315 | :query ids-q) 316 | ids (fetch-column db ids-q "id") 317 | cnt (count ids) 318 | _ (log/info :fn "import-table" 319 | :msg (str "Fetched " cnt " ids")) 320 | data-query (str "SELECT " (sql/aliased-fields-statement col-aliases) 321 | " " (sql/from-statement grouped false)) 322 | many-rels (get-in grouped [:relationships :many])] 323 | (with-meta 324 | ((fn next-batch 325 | ([batches] 326 | (next-batch batches 0)) 327 | ([batches position] 328 | (let [cur (first batches)] 329 | (if-not (seq cur) 330 | (list) 331 | (lazy-seq 332 | (let [cur-query (str data-query " WHERE " 333 | (sql/in-statement {:field id-col 334 | :vals cur})) 335 | _ (log/debug :fn "import-table" 336 | :msg "Fetching next chunk of ids" 337 | :query cur-query 338 | :position position 339 | :total cnt) 340 | table-data (fetch-results db [cur-query col-aliases]) 341 | entity-data 342 | (reduce (fn [data rel] 343 | (if-let [rel-data (import-many-relationship db rel data)] 344 | (join-results rel data rel-data) 345 | data)) 346 | table-data 347 | many-rels)] 348 | (concat entity-data (next-batch (rest batches) 349 | (+ position batch))))))))) 350 | (partition-all batch ids)) 351 | {:total cnt})))) 352 | 353 | (defn import-record 354 | "Like import-table, but returns single record from given db." 355 | [db table-spec entid] 356 | (let [{:keys [spec grouped]} table-spec 357 | col-aliases (sql/group-column-mappings grouped) 358 | id-col (table-id grouped) 359 | ent-query (str "SELECT " (sql/aliased-fields-statement col-aliases) 360 | " " (sql/from-statement grouped false) 361 | " WHERE " (sql/condition-sql {:column id-col 362 | :value entid})) 363 | many-rels (get-in grouped [:relationships :many]) 364 | ent-data (fetch-results db [ent-query col-aliases])] 365 | (first 366 | (reduce (fn [data rel] 367 | (if-let [rel-data (import-many-relationship db rel data)] 368 | (join-results rel data rel-data) 369 | data)) 370 | ent-data 371 | many-rels)))) 372 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sqlium 2 | 3 | A Datomic-inspired Clojure library for entity extraction (Big E in ETL) from a 4 | relational database. 5 | 6 | ## Rationale 7 | 8 | ### The Rectangular Prison 9 | 10 | Give credit where credit is due: the relational algebra is pretty 11 | great. It's solid, relatively easy to reason about, and we've put 50 12 | years and billions of dollars of engineering effort into making good 13 | relational database implementations. 14 | 15 | The relational model's great strength is in its unifying abstraction, 16 | the relation. Basically a table with named columns. This is also its 17 | great weakness: everything is a table. Your data is a table, and the 18 | answer to any question you ask the database will be a table; a 19 | rectangular prison that you cannot escape. 20 | 21 | The pervasive rectangleness of relational data is somewhat 22 | inconvenient for us as Clojure developers, since most interesting data 23 | that we work with is tree-shaped, not rectangle-shaped. It's so easy 24 | and natural for us to create and work with nested data structures, and 25 | they are often the most natural representation of the data models we 26 | work with. 27 | 28 | For example, we'll say we have a database of music recordings, and 29 | we're interested in albums. An album has one or more album artists - 30 | which could be bands that have one or more members - and one or more 31 | tracks each of which has properties including one or more artists, 32 | possibly one or more songwriters, possibly one or more producers, etc. 33 | 34 | The natural representation of this kind of data for us is something 35 | like: 36 | 37 | ``` clojure 38 | {:name "Abbey Road" 39 | :artist [{:name "The Beatles"}] 40 | :tracks [{:name "Come Together" 41 | :number 1 42 | :artist [{:name "The Beatles"}] 43 | :songwriter [{:name "John Lennon"} 44 | {:name "Paul McCartney"}] 45 | :producer [{:name "George Martin"}]} 46 | {:name "Something" 47 | :number 2 48 | :artist [{:name "The Beatles"}] 49 | :songwriter [{:name "George Harrison"}] 50 | ...} 51 | ... etc 52 | ]} 53 | ``` 54 | 55 | But me tell you what, it sure is annoying to get to this point if 56 | you're starting from a relational database. Because, again, it's the 57 | rectangles. Whenever you're dealing with nested zero-or-more things, 58 | you can't get it all at once. You have to do a bunch of queries to get 59 | little rectangular pieces and then stitch them together into a tree 60 | yourself. 61 | 62 | A typical way to model this in a relational database might have the 63 | following tables: 64 | 65 | - albums 66 | - artists 67 | - album_artists 68 | - tracks 69 | - album_tracks 70 | - track_artists 71 | - track_songwriters 72 | - track_producers 73 | 74 | To get "Abbey Road", we'd need to do the following queries: 75 | 76 | 1. query the albums table 77 | 2. query the album_artists table to get our list of album artists 78 | 3. query the album_tracks table joined with the tracks table to get 79 | the tracks 80 | 4. query the track_artists table with all the track ids 81 | 5. query the track_songwriters table with all the track ids 82 | 6. query the track_producers table with all the track ids 83 | 7. collect all the artist ids from the above results and get them all 84 | from the artists table 85 | 86 | After doing all these queries, we need take the data we get back and 87 | put it together in our application with code that understands each 88 | relationhsip and how it goes into the output. 89 | 90 | While it's true that there are "ORM" tools that let you do this 91 | without writing all the code yourself, let's just say it's a matter of 92 | some controversy whether they deliver on their promises or 93 | meaningfully improve the experience of building systems. 94 | 95 | ### Ideas From DJ Decomplexion 96 | 97 | Fortunately we have our own approach to databases called Datomic, 98 | designed by our very own DJ Decomplexion Rich Hickey. Datomic's 99 | approach to entities is much closer to how we treat them in our 100 | application code, and it includes a great little interface called 101 | "Pull API" for, well, pulling them out. 102 | 103 | In Pull syntax, we can grab our entire album entity with one call, 104 | using a declarative system for describing the entity and its nested 105 | pieces. It might look something like this: 106 | 107 | ``` clojure 108 | [:album/name 109 | {:album/artist [:artist/name]} 110 | {:album/track 111 | [:track/name :track/number 112 | {:track/artist [:artist/name]} 113 | {:track/songwriter [:artist/name]} 114 | {:track/producer [:artist/name]}]}] 115 | ``` 116 | 117 | It's fantastically shorter, simpler, clearer, more direct. What if we 118 | could do something like with in a relational database? 119 | 120 | sqlium provides a DSL to do just that. 121 | 122 | ## sqlium DSL 123 | 124 | In sqlium, the equivalent to the above Pull syntax example to get the 125 | contents of an album might look like this: 126 | 127 | ``` clojure 128 | (Table albums 129 | :id "album_id" 130 | :fields "name" 131 | {["_album_id" :as "artists"] 132 | (Table album_artists 133 | :id "album_artist_id" 134 | :fields 135 | {["artist_id" :flatten] 136 | (Table artists 137 | :id "artist_id" 138 | :fields "name")})} 139 | {["_album_id" :as "tracks" 140 | (Table album_tracks 141 | :id "album_track_id" 142 | :fields 143 | {["track_id" :flatten] 144 | (Table tracks 145 | :id "track_id" 146 | :fields "name" "number" 147 | {["_track_id" :as "artists"] 148 | (Table track_artists 149 | :id "track_artist_id" 150 | :fields 151 | {["artist_id" :flatten] 152 | (Table artists 153 | :id "artist_id" 154 | :fields "name")})} 155 | {["_track_id" :as "songwriters"] 156 | (Table track_songwriters 157 | :id "track_songwriter_id" 158 | :fields 159 | {["artist_id" :flatten] 160 | (Table artists 161 | :id "artist_id" 162 | :fields "name")})} 163 | {["_track_id" :as "producers"] 164 | (Table track_producers 165 | :id "track_producer_id" 166 | :fields 167 | {["artist_id" :flatten] 168 | (Table artists 169 | :id "artist_id" 170 | :fields "name")})})})]}) 171 | ``` 172 | 173 | Here's a piece-by-piece explanation of the syntax. 174 | 175 | ### Table spec 176 | 177 | This is the basic building block of sqlium's DSL. It describes some 178 | basic information about a databse table, the data that we're 179 | interested in from the table, and relationships with other tables that 180 | we are interested in. 181 | 182 | ```clojure 183 | (Table name id-spec? :fields data-spec+) 184 | ``` 185 | 186 | The table's name is a symbol, which should match the name of the table 187 | in the database. 188 | 189 | sqlium uses the idea of identity (aka primary key) columns throughout, 190 | specified in the id-spec. This is optional because it has a default 191 | value of `_id`. If your database uses a different naming 192 | convention or otherwise has custom names for identiy columns, then you 193 | need to provide it for each table. 194 | 195 | An id-spec looks like: `:id "id-column-name"`. 196 | 197 | ### Data specs 198 | 199 | A data spec is either a field or a relationship. 200 | 201 | #### Fields 202 | 203 | Fields describe the data columns from the table that you're interested 204 | in. They come in two forms. Simple field specs are just strings that 205 | match the name of the column in the table. No transformations will be 206 | applied to the column name or the data for simple field specs - the 207 | name will be keywordified in the output map but otherwise will be 208 | returned verbatim. 209 | 210 | You can also specify several options in a complex field spec. It looks like: 211 | 212 | ```clojure 213 | [col-name alias? transform?] 214 | ``` 215 | 216 | Aliasing sets the key name in the output map for a given field. Alias 217 | is specified with an :as keyword and a string alias name. For example, 218 | `["oldname" :as "newname"]` will add a `:newname` key to the output 219 | map with the contents of the "oldname" column. Nested aliases are also 220 | supported using a "." separator. For example, `["oldname" :as 221 | "new.name"]` will nest the value of the "oldname" column as in 222 | `(assoc-in output-map [:new :name] oldname-val)`. 223 | 224 | sqlium supports a number of simple data transforms. A transform can be 225 | either a function literal or reference to a symbol that's in scope for 226 | the `org.purefn.sqlium.transform` namespace. This namespace includes a 227 | built-in transform called `binary-string` which handles UTF-8 strings 228 | encoded as BLOB/binary in the database. The clojure.set namespace is 229 | also available as `set`, and the clojure.string namespace is available 230 | as `str`. 231 | 232 | Using code from your own namespaces in sqlium transforms is not 233 | supported at this time. Generally sqlium transforms are meant to be 234 | for quite simple typecasts, filtering, or replacement operations, and 235 | for more involved transformations you should generally perform them on 236 | the returned entity. 237 | 238 | nil values will not be added to the output. 239 | 240 | #### Relationships 241 | 242 | A relationship is how nested tables are added to an entity - aka a 243 | "join". 244 | 245 | sqlium understands two types of relationships: one-to-one, and one-to-many. 246 | 247 | A one-to-one relationship is one in which the some column in the 248 | parent table points to the id column of the child table. Since sqlium 249 | assumes that id columns are unique, there will only be one row in the 250 | child table for the relationship. If an id column in a one-to-one 251 | relatinship is not unique, you'll get a cartesian product and multiple 252 | entities will be returned. 253 | 254 | A one-to-many relationship is one in which some column in the child 255 | table points to the id column of the parent table. These will always 256 | be returned as collections. 257 | 258 | Relationships are specified as maps with one key, in this form: 259 | 260 | ```clojure 261 | {join-spec table-spec} 262 | ``` 263 | 264 | A `join-spec` is has two different forms, for one-to-one and 265 | one-to-many joins. 266 | 267 | One-to-one: 268 | 269 | ```clojure 270 | [foreign-key flatten-or-alias?] 271 | ``` 272 | 273 | The `foreign-key` for a one-to-one relationship is the name of the 274 | column in the parent table. One-to-one relationships can also be 275 | flattened with a `:flatten` keyword, which means that the data from 276 | the child table will be merged up into the parent table. Otherwise, 277 | you can specify the key that data from the relationship will be 278 | returned in with an `:as` keyword, just like in a field spec. If no 279 | alias is specified, the data will be returned in the keyword form of 280 | the foreign key name. 281 | 282 | One-to-many: 283 | 284 | ```clojure 285 | [foreign-key alias?] 286 | ``` 287 | 288 | In a one-to-many relationship, the foreign key is prefixed by an 289 | underscore, denoting that it's a column in the child table. This was 290 | borrowed from Datomic's syntax for specifying reverse references, 291 | which is the best defense I can offer against people who don't know 292 | Datomic and don't understand why an underscore would be used for this 293 | purpose. 294 | 295 | Optionally, an alias can be supplied. You probably want to supply an 296 | alias in almost every case, because otherwise the data will be 297 | returned in a `_{parent-id}` key which is pretty strange looking. 298 | 299 | ## Usage 300 | 301 | [API Docs](https://purefnorg.github.io/sqlium/) 302 | 303 | The two main API functions are in the org.purefn.sqlium namespace. 304 | 305 | ```clj 306 | (require '[org.purefn.sqlium :as sqlium]) 307 | ``` 308 | 309 | You can retrieve a single entity with the `entity` function. This is 310 | particularly useful for development and occasional production use - 311 | although it's not lightning fast. (Performance improvements coming!) 312 | 313 | ```clj 314 | ;; simple album spec with the album name and artist names 315 | (def album-spec 316 | ;; specs are quoted 317 | '(Table albums 318 | :id "album_id" 319 | :fields "name" 320 | {["_album_id" :as "artists"] 321 | (Table album_artists 322 | :id "album_artist_id" 323 | :fields 324 | {["artist_id" :flatten] 325 | (Table artists 326 | :id "artist_id" 327 | :fields "name")})})) 328 | 329 | ;; sqlium takes anything that clojure.java.jdbc can use as a database 330 | (def db 331 | {:dbtype "mysql" 332 | :dbname "music" 333 | :user "sqlium" 334 | :password "sqlium"}) 335 | 336 | ;; assuming database id of "Abbey Road" is 12 337 | (sqlium/entity db album-spec 12) 338 | ;; => 339 | ;; {:name "Abbey Road" 340 | ;; :artists [{:name "John"} 341 | ;; {:name "Paul"} 342 | ;; {:name "George"} 343 | ;; {:name "Ringo"}]} 344 | ``` 345 | 346 | The main use case for sqlium is extracting a lot of entities, done 347 | through the `records` function. It takes a database, spec, and some 348 | options to control what entities get returned. There are three 349 | different ways to do this. 350 | 351 | 1. Entity age 352 | 2. Entities with updates 353 | 3. A table with rows pointing to records to get updated. 354 | 355 | Here's what the docstring says about it: 356 | 357 | ```clj 358 | (defn records 359 | "Returns a lazy sequence of records for spec, querying from jdbc 360 | datasource db. Takes optional parameters as kwargs or a map to 361 | either control expiry, return updated data since a given date by 362 | comparing against provided date time fields, or return updated data 363 | based on a specific update table. Only the highest-precedence 364 | option present will be used. In order of precedence: 365 | 366 | :update - a map with: 367 | * :table string name of the update table 368 | * :id string name of the column containing entity ids to update 369 | * :updated string name of the field containing the entity update time 370 | * :date anything that can be coerced to a DateTime; the records 371 | returned will be newer than this date 372 | 373 | :delta - a map with: 374 | * :fields collection of :table/column datetime fields which will 375 | be compared with :date to detect updated data 376 | * :date anything that can be coerced to a DateTime; the records 377 | returned will be newer than this date 378 | 379 | :expiry - a map with: 380 | * :field :table/column keyword for the datetime field that 381 | determines the age of the entity 382 | * :age maximum age before the entity is ignored, either as an 383 | integer number of days or an expiration date as something 384 | that can be coerced to a DateTime" 385 | [db spec & options] 386 | ) 387 | ``` 388 | 389 | ## License 390 | 391 | Copyright © 2018 Ladders, PureFn 392 | 393 | Distributed under the Eclipse Public License either version 1.0 or (at 394 | your option) any later version. 395 | -------------------------------------------------------------------------------- /docs/js/jquery.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.11.0 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ 2 | !function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k="".trim,l={},m="1.11.0",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(l.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:k&&!k.call("\ufeff\xa0")?function(a){return null==a?"":k.call(a)}:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||n.guid++,e):void 0},now:function(){return+new Date},support:l}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s="sizzle"+-new Date,t=a.document,u=0,v=0,w=eb(),x=eb(),y=eb(),z=function(a,b){return a===b&&(j=!0),0},A="undefined",B=1<<31,C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=D.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",M=L.replace("w","w#"),N="\\["+K+"*("+L+")"+K+"*(?:([*^$|!~]?=)"+K+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+M+")|)|)"+K+"*\\]",O=":("+L+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+N.replace(3,8)+")*)|.*)\\)|)",P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(O),U=new RegExp("^"+M+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L.replace("w","w*")+")"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=/'|\\/g,ab=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),bb=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{G.apply(D=H.call(t.childNodes),t.childNodes),D[t.childNodes.length].nodeType}catch(cb){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function db(a,b,d,e){var f,g,h,i,j,m,p,q,u,v;if((b?b.ownerDocument||b:t)!==l&&k(b),b=b||l,d=d||[],!a||"string"!=typeof a)return d;if(1!==(i=b.nodeType)&&9!==i)return[];if(n&&!e){if(f=Z.exec(a))if(h=f[1]){if(9===i){if(g=b.getElementById(h),!g||!g.parentNode)return d;if(g.id===h)return d.push(g),d}else if(b.ownerDocument&&(g=b.ownerDocument.getElementById(h))&&r(b,g)&&g.id===h)return d.push(g),d}else{if(f[2])return G.apply(d,b.getElementsByTagName(a)),d;if((h=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(h)),d}if(c.qsa&&(!o||!o.test(a))){if(q=p=s,u=b,v=9===i&&a,1===i&&"object"!==b.nodeName.toLowerCase()){m=ob(a),(p=b.getAttribute("id"))?q=p.replace(_,"\\$&"):b.setAttribute("id",q),q="[id='"+q+"'] ",j=m.length;while(j--)m[j]=q+pb(m[j]);u=$.test(a)&&mb(b.parentNode)||b,v=m.join(",")}if(v)try{return G.apply(d,u.querySelectorAll(v)),d}catch(w){}finally{p||b.removeAttribute("id")}}}return xb(a.replace(P,"$1"),b,d,e)}function eb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function fb(a){return a[s]=!0,a}function gb(a){var b=l.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function hb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function ib(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||B)-(~a.sourceIndex||B);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function jb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function kb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function lb(a){return fb(function(b){return b=+b,fb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function mb(a){return a&&typeof a.getElementsByTagName!==A&&a}c=db.support={},f=db.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},k=db.setDocument=function(a){var b,e=a?a.ownerDocument||a:t,g=e.defaultView;return e!==l&&9===e.nodeType&&e.documentElement?(l=e,m=e.documentElement,n=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){k()},!1):g.attachEvent&&g.attachEvent("onunload",function(){k()})),c.attributes=gb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=gb(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(e.getElementsByClassName)&&gb(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=gb(function(a){return m.appendChild(a).id=s,!e.getElementsByName||!e.getElementsByName(s).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==A&&n){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ab,bb);return function(a){var c=typeof a.getAttributeNode!==A&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==A?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==A&&n?b.getElementsByClassName(a):void 0},p=[],o=[],(c.qsa=Y.test(e.querySelectorAll))&&(gb(function(a){a.innerHTML="",a.querySelectorAll("[t^='']").length&&o.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||o.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll(":checked").length||o.push(":checked")}),gb(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&o.push("name"+K+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||o.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),o.push(",.*:")})),(c.matchesSelector=Y.test(q=m.webkitMatchesSelector||m.mozMatchesSelector||m.oMatchesSelector||m.msMatchesSelector))&&gb(function(a){c.disconnectedMatch=q.call(a,"div"),q.call(a,"[s!='']:x"),p.push("!=",O)}),o=o.length&&new RegExp(o.join("|")),p=p.length&&new RegExp(p.join("|")),b=Y.test(m.compareDocumentPosition),r=b||Y.test(m.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},z=b?function(a,b){if(a===b)return j=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===t&&r(t,a)?-1:b===e||b.ownerDocument===t&&r(t,b)?1:i?I.call(i,a)-I.call(i,b):0:4&d?-1:1)}:function(a,b){if(a===b)return j=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],k=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:i?I.call(i,a)-I.call(i,b):0;if(f===g)return ib(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)k.unshift(c);while(h[d]===k[d])d++;return d?ib(h[d],k[d]):h[d]===t?-1:k[d]===t?1:0},e):l},db.matches=function(a,b){return db(a,null,null,b)},db.matchesSelector=function(a,b){if((a.ownerDocument||a)!==l&&k(a),b=b.replace(S,"='$1']"),!(!c.matchesSelector||!n||p&&p.test(b)||o&&o.test(b)))try{var d=q.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return db(b,l,null,[a]).length>0},db.contains=function(a,b){return(a.ownerDocument||a)!==l&&k(a),r(a,b)},db.attr=function(a,b){(a.ownerDocument||a)!==l&&k(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!n):void 0;return void 0!==f?f:c.attributes||!n?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},db.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},db.uniqueSort=function(a){var b,d=[],e=0,f=0;if(j=!c.detectDuplicates,i=!c.sortStable&&a.slice(0),a.sort(z),j){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return i=null,a},e=db.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=db.selectors={cacheLength:50,createPseudo:fb,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ab,bb),a[3]=(a[4]||a[5]||"").replace(ab,bb),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||db.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&db.error(a[0]),a},PSEUDO:function(a){var b,c=!a[5]&&a[2];return V.CHILD.test(a[0])?null:(a[3]&&void 0!==a[4]?a[2]=a[4]:c&&T.test(c)&&(b=ob(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ab,bb).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=w[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&w(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==A&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=db.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),t=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&t){k=q[s]||(q[s]={}),j=k[a]||[],n=j[0]===u&&j[1],m=j[0]===u&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[u,n,m];break}}else if(t&&(j=(b[s]||(b[s]={}))[a])&&j[0]===u)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(t&&((l[s]||(l[s]={}))[a]=[u,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||db.error("unsupported pseudo: "+a);return e[s]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?fb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:fb(function(a){var b=[],c=[],d=g(a.replace(P,"$1"));return d[s]?fb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:fb(function(a){return function(b){return db(a,b).length>0}}),contains:fb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:fb(function(a){return U.test(a||"")||db.error("unsupported lang: "+a),a=a.replace(ab,bb).toLowerCase(),function(b){var c;do if(c=n?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===m},focus:function(a){return a===l.activeElement&&(!l.hasFocus||l.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:lb(function(){return[0]}),last:lb(function(a,b){return[b-1]}),eq:lb(function(a,b,c){return[0>c?c+b:c]}),even:lb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:lb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:lb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:lb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function qb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=v++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[u,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[s]||(b[s]={}),(h=i[d])&&h[0]===u&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function rb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function sb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function tb(a,b,c,d,e,f){return d&&!d[s]&&(d=tb(d)),e&&!e[s]&&(e=tb(e,f)),fb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||wb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:sb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=sb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=sb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ub(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],i=g||d.relative[" "],j=g?1:0,k=qb(function(a){return a===b},i,!0),l=qb(function(a){return I.call(b,a)>-1},i,!0),m=[function(a,c,d){return!g&&(d||c!==h)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>j;j++)if(c=d.relative[a[j].type])m=[qb(rb(m),c)];else{if(c=d.filter[a[j].type].apply(null,a[j].matches),c[s]){for(e=++j;f>e;e++)if(d.relative[a[e].type])break;return tb(j>1&&rb(m),j>1&&pb(a.slice(0,j-1).concat({value:" "===a[j-2].type?"*":""})).replace(P,"$1"),c,e>j&&ub(a.slice(j,e)),f>e&&ub(a=a.slice(e)),f>e&&pb(a))}m.push(c)}return rb(m)}function vb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,i,j,k){var m,n,o,p=0,q="0",r=f&&[],s=[],t=h,v=f||e&&d.find.TAG("*",k),w=u+=null==t?1:Math.random()||.1,x=v.length;for(k&&(h=g!==l&&g);q!==x&&null!=(m=v[q]);q++){if(e&&m){n=0;while(o=a[n++])if(o(m,g,i)){j.push(m);break}k&&(u=w)}c&&((m=!o&&m)&&p--,f&&r.push(m))}if(p+=q,c&&q!==p){n=0;while(o=b[n++])o(r,s,g,i);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=E.call(j));s=sb(s)}G.apply(j,s),k&&!f&&s.length>0&&p+b.length>1&&db.uniqueSort(j)}return k&&(u=w,h=t),r};return c?fb(f):f}g=db.compile=function(a,b){var c,d=[],e=[],f=y[a+" "];if(!f){b||(b=ob(a)),c=b.length;while(c--)f=ub(b[c]),f[s]?d.push(f):e.push(f);f=y(a,vb(e,d))}return f};function wb(a,b,c){for(var d=0,e=b.length;e>d;d++)db(a,b[d],c);return c}function xb(a,b,e,f){var h,i,j,k,l,m=ob(a);if(!f&&1===m.length){if(i=m[0]=m[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&c.getById&&9===b.nodeType&&n&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(ab,bb),b)||[])[0],!b)return e;a=a.slice(i.shift().value.length)}h=V.needsContext.test(a)?0:i.length;while(h--){if(j=i[h],d.relative[k=j.type])break;if((l=d.find[k])&&(f=l(j.matches[0].replace(ab,bb),$.test(i[0].type)&&mb(b.parentNode)||b))){if(i.splice(h,1),a=f.length&&pb(i),!a)return G.apply(e,f),e;break}}}return g(a,m)(f,b,!n,e,$.test(a)&&mb(b.parentNode)||b),e}return c.sortStable=s.split("").sort(z).join("")===s,c.detectDuplicates=!!j,k(),c.sortDetached=gb(function(a){return 1&a.compareDocumentPosition(l.createElement("div"))}),gb(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||hb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&gb(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||hb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),gb(function(a){return null==a.getAttribute("disabled")})||hb(J,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),db}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=a.document,A=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,B=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:A.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:z,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=z.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return y.find(a);this.length=1,this[0]=d}return this.context=z,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};B.prototype=n.fn,y=n(z);var C=/^(?:parents|prev(?:Until|All))/,D={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!n(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function E(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return E(a,"nextSibling")},prev:function(a){return E(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(D[a]||(e=n.unique(e)),C.test(a)&&(e=e.reverse())),this.pushStack(e)}});var F=/\S+/g,G={};function H(a){var b=G[a]={};return n.each(a.match(F)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?G[a]||H(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&n.each(arguments,function(a,c){var d;while((d=n.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){if(a===!0?!--n.readyWait:!n.isReady){if(!z.body)return setTimeout(n.ready);n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(z,[n]),n.fn.trigger&&n(z).trigger("ready").off("ready"))}}});function J(){z.addEventListener?(z.removeEventListener("DOMContentLoaded",K,!1),a.removeEventListener("load",K,!1)):(z.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(z.addEventListener||"load"===event.type||"complete"===z.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===z.readyState)setTimeout(n.ready);else if(z.addEventListener)z.addEventListener("DOMContentLoaded",K,!1),a.addEventListener("load",K,!1);else{z.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&z.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!n.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}J(),n.ready()}}()}return I.promise(b)};var L="undefined",M;for(M in n(l))break;l.ownLast="0"!==M,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c=z.getElementsByTagName("body")[0];c&&(a=z.createElement("div"),a.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",b=z.createElement("div"),c.appendChild(a).appendChild(b),typeof b.style.zoom!==L&&(b.style.cssText="border:0;margin:0;width:1px;padding:1px;display:inline;zoom:1",(l.inlineBlockNeedsLayout=3===b.offsetWidth)&&(c.style.zoom=1)),c.removeChild(a),a=b=null)}),function(){var a=z.createElement("div");if(null==l.deleteExpando){l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}}a=null}(),n.acceptData=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(n.acceptData(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f 3 | }}function S(a,b,c){if(n.acceptData(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d]));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},X=/^(?:checkbox|radio)$/i;!function(){var a=z.createDocumentFragment(),b=z.createElement("div"),c=z.createElement("input");if(b.setAttribute("className","t"),b.innerHTML="
a",l.leadingWhitespace=3===b.firstChild.nodeType,l.tbody=!b.getElementsByTagName("tbody").length,l.htmlSerialize=!!b.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==z.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,a.appendChild(c),l.appendChecked=c.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,a.appendChild(b),b.innerHTML="",l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){l.noCloneEvent=!1}),b.cloneNode(!0).click()),null==l.deleteExpando){l.deleteExpando=!0;try{delete b.test}catch(d){l.deleteExpando=!1}}a=b=c=null}(),function(){var b,c,d=z.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),l[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var Y=/^(?:input|select|textarea)$/i,Z=/^key/,$=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,ab=/^([^.]*)(?:\.(.+)|)$/;function bb(){return!0}function cb(){return!1}function db(){try{return z.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof n===L||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(F)||[""],h=b.length;while(h--)f=ab.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(F)||[""],j=b.length;while(j--)if(h=ab.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,m,o=[d||z],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||z,3!==d.nodeType&&8!==d.nodeType&&!_.test(p+n.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[n.expando]?b:new n.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),k=n.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!n.isWindow(d)){for(i=k.delegateType||p,_.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||z)&&o.push(l.defaultView||l.parentWindow||a)}m=0;while((h=o[m++])&&!b.isPropagationStopped())b.type=m>1?i:k.bindType||p,f=(n._data(h,"events")||{})[b.type]&&n._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&n.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&n.acceptData(d)&&g&&d[p]&&!n.isWindow(d)){l=d[g],l&&(d[g]=null),n.event.triggered=p;try{d[p]()}catch(r){}n.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((n.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?n(c,this).index(i)>=0:n.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),ib=/^\s+/,jb=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,kb=/<([\w:]+)/,lb=/\s*$/g,sb={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:l.htmlSerialize?[0,"",""]:[1,"X
","
"]},tb=eb(z),ub=tb.appendChild(z.createElement("div"));sb.optgroup=sb.option,sb.tbody=sb.tfoot=sb.colgroup=sb.caption=sb.thead,sb.th=sb.td;function vb(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==L?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==L?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,vb(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function wb(a){X.test(a.type)&&(a.defaultChecked=a.checked)}function xb(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function yb(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function zb(a){var b=qb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ab(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}function Bb(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Cb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(yb(b).text=a.text,zb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&X.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}n.extend({clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!hb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(ub.innerHTML=a.outerHTML,ub.removeChild(f=ub.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=vb(f),h=vb(a),g=0;null!=(e=h[g]);++g)d[g]&&Cb(e,d[g]);if(b)if(c)for(h=h||vb(a),d=d||vb(f),g=0;null!=(e=h[g]);g++)Bb(e,d[g]);else Bb(a,f);return d=vb(f,"script"),d.length>0&&Ab(d,!i&&vb(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k,m=a.length,o=eb(b),p=[],q=0;m>q;q++)if(f=a[q],f||0===f)if("object"===n.type(f))n.merge(p,f.nodeType?[f]:f);else if(mb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(kb.exec(f)||["",""])[1].toLowerCase(),k=sb[i]||sb._default,h.innerHTML=k[1]+f.replace(jb,"<$1>")+k[2],e=k[0];while(e--)h=h.lastChild;if(!l.leadingWhitespace&&ib.test(f)&&p.push(b.createTextNode(ib.exec(f)[0])),!l.tbody){f="table"!==i||lb.test(f)?""!==k[1]||lb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)n.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}n.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),l.appendChecked||n.grep(vb(p,"input"),wb),q=0;while(f=p[q++])if((!d||-1===n.inArray(f,d))&&(g=n.contains(f.ownerDocument,f),h=vb(o.appendChild(f),"script"),g&&Ab(h),c)){e=0;while(f=h[e++])pb.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.deleteExpando,m=n.event.special;null!=(d=a[h]);h++)if((b||n.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k?delete d[i]:typeof d.removeAttribute!==L?d.removeAttribute(i):d[i]=null,c.push(f))}}}),n.fn.extend({text:function(a){return W(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||z).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=xb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(vb(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&Ab(vb(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(vb(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return W(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(gb,""):void 0;if(!("string"!=typeof a||nb.test(a)||!l.htmlSerialize&&hb.test(a)||!l.leadingWhitespace&&ib.test(a)||sb[(kb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(jb,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(vb(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(vb(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,k=this.length,m=this,o=k-1,p=a[0],q=n.isFunction(p);if(q||k>1&&"string"==typeof p&&!l.checkClone&&ob.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(k&&(i=n.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=n.map(vb(i,"script"),yb),f=g.length;k>j;j++)d=i,j!==o&&(d=n.clone(d,!0,!0),f&&n.merge(g,vb(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,n.map(g,zb),j=0;f>j;j++)d=g[j],pb.test(d.type||"")&&!n._data(d,"globalEval")&&n.contains(h,d)&&(d.src?n._evalUrl&&n._evalUrl(d.src):n.globalEval((d.text||d.textContent||d.innerHTML||"").replace(rb,"")));i=c=null}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],g=n(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Db,Eb={};function Fb(b,c){var d=n(c.createElement(b)).appendTo(c.body),e=a.getDefaultComputedStyle?a.getDefaultComputedStyle(d[0]).display:n.css(d[0],"display");return d.detach(),e}function Gb(a){var b=z,c=Eb[a];return c||(c=Fb(a,b),"none"!==c&&c||(Db=(Db||n("