diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ac2d43c..ff42bb4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,6 +48,14 @@ jobs: toolchain: ${{ matrix.rust-version }} override: true + - name: Install libpq (Windows) + if: matrix.os == 'windows-latest' + shell: bash + run: | + choco install postgresql12 --force --params '/Password:root' + echo '::add-path::C:\Program Files\PostgreSQL\12\bin' + echo '::add-path::C:\Program Files\PostgreSQL\12\lib' + - uses: actions-rs/cargo@v1 with: command: test diff --git a/Cargo.lock b/Cargo.lock index 14835c8..77e11d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,6 +142,17 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "chrono" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" +dependencies = [ + "num-integer", + "num-traits", + "time", +] + [[package]] name = "cookie" version = "0.11.3" @@ -185,7 +196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "066ceb7928ca93a9bedc6d0e612a8a0424048b0ab1f75971b203d01420c055d7" dependencies = [ "devise_core", - "quote", + "quote 0.6.13", ] [[package]] @@ -195,9 +206,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf41c59b22b5e3ec0ea55c7847e5f358d340f3a8d6d53a5cf4f1564967f96487" dependencies = [ "bitflags", - "proc-macro2", - "quote", - "syn", + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "diesel" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c" +dependencies = [ + "bitflags", + "byteorder", + "diesel_derives", + "pq-sys", +] + +[[package]] +name = "diesel_derives" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" +dependencies = [ + "proc-macro2 1.0.19", + "quote 1.0.7", + "syn 1.0.35", ] [[package]] @@ -555,6 +589,25 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-integer" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -586,9 +639,9 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfc1c836fdc3d1ef87c348b237b5b5c4dff922156fb2d968f57734f9669768ca" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", "version_check 0.9.2", "yansi", ] @@ -621,13 +674,31 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" +[[package]] +name = "pq-sys" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda" +dependencies = [ + "vcpkg", +] + [[package]] name = "proc-macro2" version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" dependencies = [ - "unicode-xid", + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" +dependencies = [ + "unicode-xid 0.2.1", ] [[package]] @@ -636,7 +707,16 @@ version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" dependencies = [ - "proc-macro2", + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2 1.0.19", ] [[package]] @@ -716,7 +796,7 @@ dependencies = [ "devise", "glob", "indexmap", - "quote", + "quote 0.6.13", "rocket_http", "version_check 0.9.2", "yansi", @@ -749,7 +829,7 @@ dependencies = [ "smallvec", "state", "time", - "unicode-xid", + "unicode-xid 0.1.0", ] [[package]] @@ -838,9 +918,20 @@ version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7f4c519df8c117855e19dd8cc851e89eb746fe7a73f0157e0d95fdec5369b0" +dependencies = [ + "proc-macro2 1.0.19", + "quote 1.0.7", + "unicode-xid 0.2.1", ] [[package]] @@ -919,6 +1010,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + [[package]] name = "universal-hash" version = "0.3.0" @@ -940,6 +1037,12 @@ dependencies = [ "percent-encoding 1.0.1", ] +[[package]] +name = "vcpkg" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" + [[package]] name = "version_check" version = "0.1.5" @@ -1026,6 +1129,8 @@ dependencies = [ name = "xsnippet-api" version = "5.0.0" dependencies = [ + "chrono", + "diesel", "rocket", "rocket_contrib", ] diff --git a/Cargo.toml b/Cargo.toml index 37205e7..328dc93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,7 @@ categories = [ publish = false [dependencies] +chrono = "0.4.13" +diesel = { version = "1.4.5", features = ["postgres"] } rocket = "0.4.5" rocket_contrib = {version = "0.4.5", features = ["json"]} diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..104421e --- /dev/null +++ b/diesel.toml @@ -0,0 +1,8 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/storage/sql/schema.rs" + +[migrations_directory] +dir = "src/storage/sql/migrations" diff --git a/src/main.rs b/src/main.rs index cfd0e27..f5d7906 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ #![feature(proc_macro_hygiene, decl_macro)] +#[macro_use] +extern crate diesel; #[macro_use] extern crate rocket; #[macro_use] @@ -12,6 +14,7 @@ extern crate rocket_contrib; mod application; mod routes; +mod storage; fn main() { let app = application::create_app(); diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..5d37751 --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,3 @@ +// Do not fail compilation until we've started using that code +#[allow(dead_code)] +pub mod sql; diff --git a/src/storage/sql/migrations/2020-07-25-065258_create_snippets/down.sql b/src/storage/sql/migrations/2020-07-25-065258_create_snippets/down.sql new file mode 100644 index 0000000..eac66f2 --- /dev/null +++ b/src/storage/sql/migrations/2020-07-25-065258_create_snippets/down.sql @@ -0,0 +1,3 @@ +DROP TABLE tags; +DROP TABLE changesets; +DROP TABLE snippets; diff --git a/src/storage/sql/migrations/2020-07-25-065258_create_snippets/up.sql b/src/storage/sql/migrations/2020-07-25-065258_create_snippets/up.sql new file mode 100644 index 0000000..86584ca --- /dev/null +++ b/src/storage/sql/migrations/2020-07-25-065258_create_snippets/up.sql @@ -0,0 +1,59 @@ +CREATE TABLE snippets ( + -- an internal autoincrementing identifier used in foreign keys. + -- Normally not visible publicly, except for the case when it is + -- used for looking legacy snippets up by id + id SERIAL PRIMARY KEY, + -- a short unique snippet identifier visible to users + slug VARCHAR(32) NOT NULL, + + title TEXT, + syntax TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + + -- slugs must be unique (this will also automatically create a unique index) + CONSTRAINT uq_slug UNIQUE (slug) +); + + +-- will be used for pagination; slug guarantees uniqueness of the sorting key +CREATE INDEX snippets_created_at_slug ON snippets (created_at, slug); +CREATE INDEX snippets_updated_at_slug ON snippets (updated_at, slug); + + +CREATE TABLE changesets ( + id SERIAL PRIMARY KEY, + snippet_id INTEGER NOT NULL, + + -- numeric index used to determine the ordering of changesets for a given snippet + version INTEGER DEFAULT 0 NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, + + -- there can be multiple changesets per snippet + CONSTRAINT fk_snippet FOREIGN KEY (snippet_id) REFERENCES snippets(id), + -- but each one is supposed to have a unique version number + CONSTRAINT uq_version UNIQUE (snippet_id, version), + -- sanity check: do not allow empty changesets + CONSTRAINT check_not_empty CHECK (LENGTH(content) > 0), + -- sanity check: version numbers are non-negative integers + CONSTRAINT check_non_negative_version CHECK (version >= 0) +); + + +-- tags could have been associated with snippets as M:M via an auxiliary table, +-- but Diesel only supports child-parent associations, so let's do that instead +CREATE TABLE tags ( + id SERIAL PRIMARY KEY, + snippet_id INTEGER NOT NULL, + + value TEXT NOT NULL, + + -- there can be multiple tags per snippet + CONSTRAINT fk_snippet FOREIGN KEY (snippet_id) REFERENCES snippets(id), + -- do not allow to abuse the tags for storing too much data + CONSTRAINT check_length CHECK (LENGTH(value) < 128), + -- do not allow repeated tags per snippet + CONSTRAINT uq_snippet_tag UNIQUE (snippet_id, value) +); diff --git a/src/storage/sql/mod.rs b/src/storage/sql/mod.rs new file mode 100644 index 0000000..6bde67a --- /dev/null +++ b/src/storage/sql/mod.rs @@ -0,0 +1 @@ +mod schema; diff --git a/src/storage/sql/schema.rs b/src/storage/sql/schema.rs new file mode 100644 index 0000000..eeaaedd --- /dev/null +++ b/src/storage/sql/schema.rs @@ -0,0 +1,36 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + changesets (id) { + id -> Int4, + snippet_id -> Int4, + version -> Int4, + content -> Text, + created_at -> Timestamptz, + updated_at -> Timestamptz, + } +} + +diesel::table! { + snippets (id) { + id -> Int4, + slug -> Varchar, + title -> Nullable, + syntax -> Nullable, + created_at -> Timestamptz, + updated_at -> Timestamptz, + } +} + +diesel::table! { + tags (id) { + id -> Int4, + snippet_id -> Int4, + value -> Text, + } +} + +diesel::joinable!(changesets -> snippets (snippet_id)); +diesel::joinable!(tags -> snippets (snippet_id)); + +diesel::allow_tables_to_appear_in_same_query!(changesets, snippets, tags,);