Showing preview only (313K chars total). Download the full file or copy to clipboard to get everything.
Repository: amberframework/granite
Branch: master
Commit: f4711b63bacf
Files: 132
Total size: 282.1 KB
Directory structure:
gitextract_almbvcqk/
├── .dockerignore
├── .envrc
├── .github/
│ └── workflows/
│ └── spec.yml
├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── docker/
│ ├── docker-compose.mysql.yml
│ ├── docker-compose.pg.yml
│ └── docker-compose.sqlite.yml
├── docker-compose.yml
├── docs/
│ ├── callbacks.md
│ ├── crud.md
│ ├── imports.md
│ ├── migrations.md
│ ├── models.md
│ ├── multiple_connections.md
│ ├── querying.md
│ ├── readme.md
│ ├── relationships.md
│ └── validations.md
├── export.sh
├── shard.yml
├── spec/
│ ├── adapter/
│ │ ├── adapters_spec.cr
│ │ ├── mysql_spec.cr
│ │ ├── pg_spec.cr
│ │ └── sqlite_spec.cr
│ ├── granite/
│ │ ├── associations/
│ │ │ ├── belongs_to_spec.cr
│ │ │ ├── has_many_spec.cr
│ │ │ ├── has_many_through_spec.cr
│ │ │ └── has_one_spec.cr
│ │ ├── callbacks/
│ │ │ ├── abort_spec.cr
│ │ │ └── callbacks_spec.cr
│ │ ├── columns/
│ │ │ ├── primary_key_spec.cr
│ │ │ ├── read_attribute_spec.cr
│ │ │ ├── timestamps_spec.cr
│ │ │ └── uuid_spec.cr
│ │ ├── connection_management_spec.cr
│ │ ├── converters/
│ │ │ ├── converters_spec.cr
│ │ │ ├── enum_spec.cr
│ │ │ ├── json_spec.cr
│ │ │ └── pg_numeric_spec.cr
│ │ ├── error/
│ │ │ └── error_spec.cr
│ │ ├── exceptions/
│ │ │ ├── record_invalid_spec.cr
│ │ │ └── record_not_destroyed_spec.cr
│ │ ├── integrators/
│ │ │ └── find_or_spec.cr
│ │ ├── migrator/
│ │ │ └── migrator_spec.cr
│ │ ├── query/
│ │ │ ├── assemblers/
│ │ │ │ ├── mysql_spec.cr
│ │ │ │ ├── pg_spec.cr
│ │ │ │ └── sqlite_spec.cr
│ │ │ ├── builder_spec.cr
│ │ │ ├── executor_spec.cr
│ │ │ └── spec_helper.cr
│ │ ├── querying/
│ │ │ ├── all_spec.cr
│ │ │ ├── count_spec.cr
│ │ │ ├── exists_spec.cr
│ │ │ ├── find_by_spec.cr
│ │ │ ├── find_each_spec.cr
│ │ │ ├── find_in_batches.cr
│ │ │ ├── find_spec.cr
│ │ │ ├── first_spec.cr
│ │ │ ├── from_rs_spec.cr
│ │ │ ├── passthrough_spec.cr
│ │ │ ├── query_builder_spec.cr
│ │ │ └── reload_spec.cr
│ │ ├── select/
│ │ │ └── select_spec.cr
│ │ ├── table/
│ │ │ └── table_spec.cr
│ │ ├── transactions/
│ │ │ ├── create_spec.cr
│ │ │ ├── destroy_spec.cr
│ │ │ ├── import_spec.cr
│ │ │ ├── save_natural_key_spec.cr
│ │ │ ├── save_spec.cr
│ │ │ ├── touch_spec.cr
│ │ │ └── update_spec.cr
│ │ ├── validation_helpers/
│ │ │ ├── blank_spec.cr
│ │ │ ├── choice_spec.cr
│ │ │ ├── exclusion_spec.cr
│ │ │ ├── inequality_spec.cr
│ │ │ ├── lenght_spec.cr
│ │ │ ├── nil_spec.cr
│ │ │ └── uniqueness_spec.cr
│ │ └── validations/
│ │ └── validator_spec.cr
│ ├── granite_spec.cr
│ ├── mocks/
│ │ └── db_mock.cr
│ ├── run_all_specs.sh
│ ├── run_test_dbs.sh
│ ├── spec_helper.cr
│ └── spec_models.cr
└── src/
├── adapter/
│ ├── base.cr
│ ├── mysql.cr
│ ├── pg.cr
│ └── sqlite.cr
├── granite/
│ ├── association_collection.cr
│ ├── associations.cr
│ ├── base.cr
│ ├── callbacks.cr
│ ├── collection.cr
│ ├── columns.cr
│ ├── connection_management.cr
│ ├── connections.cr
│ ├── converters.cr
│ ├── error.cr
│ ├── exceptions.cr
│ ├── integrators.cr
│ ├── migrator.cr
│ ├── query/
│ │ ├── assemblers/
│ │ │ ├── base.cr
│ │ │ ├── mysql.cr
│ │ │ ├── pg.cr
│ │ │ └── sqlite.cr
│ │ ├── builder.cr
│ │ ├── builder_methods.cr
│ │ └── executors/
│ │ ├── base.cr
│ │ ├── list.cr
│ │ ├── multi_value.cr
│ │ └── value.cr
│ ├── querying.cr
│ ├── select.cr
│ ├── settings.cr
│ ├── table.cr
│ ├── transactions.cr
│ ├── type.cr
│ ├── validation_helpers/
│ │ ├── blank.cr
│ │ ├── choice.cr
│ │ ├── exclusion.cr
│ │ ├── inequality.cr
│ │ ├── length.cr
│ │ ├── nil.cr
│ │ └── uniqueness.cr
│ ├── validators.cr
│ └── version.cr
└── granite.cr
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
bin
================================================
FILE: .envrc
================================================
dotenv_if_exists
================================================
FILE: .github/workflows/spec.yml
================================================
name: spec
on:
push:
pull_request:
branches: [main, master]
# schedule:
# - cron: "0 6 * * 6" # Every Saturday 6 AM
jobs:
formatting:
runs-on: ubuntu-latest
steps:
- name: Download source
uses: actions/checkout@v2
- name: Install Crystal
uses: oprypin/install-crystal@v1.8.0
with:
crystal: latest
- name: Install Ameba
run: shards install
- name: Check formatting
run: crystal tool format --check
- name: Check linting
run: ./bin/ameba
sqlite-spec:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 1
matrix:
crystal: [1.6.2, 1.7.2, 1.8.1, latest]
steps:
- name: Download source
uses: actions/checkout@v2
- name: Install Crystal
uses: oprypin/install-crystal@v1.8.0
with:
crystal: ${{ matrix.crystal }}
- name: Install shards
run: shards update --ignore-crystal-version --skip-postinstall --skip-executables
- name: Run tests
timeout-minutes: 2
run: crystal spec
env:
CURRENT_ADAPTER: sqlite
SQLITE_DATABASE_URL: sqlite3:./granite.db
SQLITE_REPLICA_URL: sqlite3:./granite_replica.db
mysql-spec:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 1
matrix:
crystal: [1.6.2, 1.7.2, 1.8.1, latest]
services:
mysql:
image: mysql:5.7
options: >-
--health-cmd "mysqladmin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: granite_db
MYSQL_USER: granite
MYSQL_PASSWORD: password
ports:
- 3306:3306
steps:
- name: Download source
uses: actions/checkout@v2
- name: Install Crystal
uses: oprypin/install-crystal@v1.8.0
with:
crystal: ${{ matrix.crystal }}
- name: Install shards
run: shards update --ignore-crystal-version --skip-postinstall --skip-executables
- name: Run tests
timeout-minutes: 2
run: crystal spec
env:
CURRENT_ADAPTER: mysql
SQLITE_DATABASE_URL: sqlite3:./granite.db
MYSQL_DATABASE_URL: mysql://granite:password@localhost:3306/granite_db
MYSQL_REPLICA_URL: mysql://granite:password@localhost:3306/granite_db
psql-spec:
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 1
matrix:
crystal: [1.6.2, 1.7.2, 1.8.1, latest]
services:
postgres:
image: postgres:15.2
env:
POSTGRES_USER: granite
POSTGRES_PASSWORD: password
POSTGRES_DB: granite_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
steps:
- name: Download source
uses: actions/checkout@v2
- name: Install Crystal
uses: oprypin/install-crystal@v1.8.0
with:
crystal: ${{ matrix.crystal }}
- name: Install shards
run: shards update --ignore-crystal-version --skip-postinstall --skip-executables
- name: Run tests
timeout-minutes: 2
run: crystal spec
env:
CURRENT_ADAPTER: pg
PG_DATABASE_URL: postgres://granite:password@localhost:5432/granite_db
PG_REPLICA_URL: postgres://granite:password@localhost:5432/granite_db
================================================
FILE: .gitignore
================================================
/lib/
/.shards/
/.deps/
/.crystal/
/doc/
*.db
# Libraries don't need dependency lock
# Dependencies will be locked in application that uses them
/.deps.lock
shard.lock
# Ignore bin because they will be build with shards install
bin
.env
================================================
FILE: .travis.yml
================================================
language: generic
services:
- docker
before_install:
- docker-compose -f docker/docker-compose.$CURRENT_ADAPTER.yml build spec
script:
- docker-compose -f docker/docker-compose.$CURRENT_ADAPTER.yml run spec
matrix:
include:
- name: "Mysql 5.7"
env:
- CURRENT_ADAPTER=mysql
- MYSQL_VERSION=5.7
- PG_VERSION=10.5
- name: "Mysql 5.6"
env:
- CURRENT_ADAPTER=mysql
- MYSQL_VERSION=5.6
- PG_VERSION=10.5
- name: "Postgres 9.6"
env:
- CURRENT_ADAPTER=pg
- MYSQL_VERSION=5.7
- PG_VERSION=9.6
- name: "Postgres 10.5"
env:
- CURRENT_ADAPTER=pg
- MYSQL_VERSION=5.7
- PG_VERSION=10.5
- name: "Postgres 11"
env:
- CURRENT_ADAPTER=pg
- MYSQL_VERSION=5.7
- PG_VERSION=11
- name: "Sqlite 3.11.0"
env:
- CURRENT_ADAPTER=sqlite
- MYSQL_VERSION=5.7
- PG_VERSION=10.5
- SQLITE_VERSION=3110000
- SQLITE_VERSION_YEAR=2016
- name: "Sqlite 3.25.2"
env:
- CURRENT_ADAPTER=sqlite
- MYSQL_VERSION=5.7
- PG_VERSION=10.5
- SQLITE_VERSION=3250200
- SQLITE_VERSION_YEAR=2018
================================================
FILE: Dockerfile
================================================
FROM 84codes/crystal:latest-ubuntu-jammy
# Install deps
RUN apt-get update -qq && apt-get install -y --no-install-recommends libpq-dev libmysqlclient-dev libsqlite3-dev
WORKDIR /app/user
COPY shard.yml /app/user
COPY shard.lock /app/user
RUN shards install
COPY src /app/user/src
COPY spec /app/user/spec
ENTRYPOINT []
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2019 dru.jensen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Granite
[Amber](https://github.com/amberframework/amber) is a web framework written in
the [Crystal](https://github.com/crystal-lang/crystal) language.
This project is to provide an ORM in Crystal.
# Looking for maintainers
Granite is looking for volunteers to take over maintainership of the repository, reviewing and merging pull requests, stewarding updates to follow along with Crystal language updates, etc. [More information here](https://github.com/amberframework/granite/issues/462)
## Documentation
[Documentation](docs/readme.md)
## Contributing
1. Fork it ( https://github.com/amberframework/granite/fork )
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Add some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create a new Pull Request
## Running tests
Granite uses Crystal's built in test framework. The tests can be run either within a [dockerized testing environment](#docker-setup) or [locally](#local-setup).
The test suite depends on access to a PostgreSQL, MySQL, and SQLite database to ensure the adapters work as intended.
### Docker setup
There is a self-contained testing environment provided via the `docker-compose.yml` file in this repository.
We are testing against multiple databases so you have to specify which docker-compose file you would like to use.
- You can find postgres versions at https://hub.docker.com/_/postgres/
- You can find mysql versions at https://hub.docker.com/_/mysql/
After you have docker installed do the following to run tests:
#### Environment variable setup
##### Option 1
Export `.env` with `$ source ./export.sh` or `$ source .env`.
##### Option 2
Modify the `.env` file that docker-compose loads by default. The `.env` file can either be copied to the same directory as the docker-compose.{database_type}.yml files or passed as an option to the docker-compose commands `--env-file ./foo/.env`.
#### First run
> Replace "{database_type}" with "mysql" or "pg" or "sqlite".
```
$ docker-compose -f docker/docker-compose.{database_type}.yml build spec
$ docker-compose -f docker/docker-compose.{database_type}.yml run spec
```
#### Subsequent runs
```
$ docker-compose -f docker/docker-compose.{database_type}.yml run spec
```
#### Cleanup
If you're done testing and you'd like to shut down and clean up the docker dependences run the following:
```
$ docker-compose -f docker/docker-compose.{database_type}.yml down
```
#### Run all
To run the specs for each database adapter use `./spec/run_all_specs.sh`. This will build and run each adapter, then cleanup after itself.
### Local setup
If you'd like to test without docker you can do so by following the instructions below:
1. Install dependencies with `$ shards install `
2. Update .env to use appropriate ENV variables, or create appropriate databases.
3. Setup databases:
#### PostgreSQL
```sql
CREATE USER granite WITH PASSWORD 'password';
CREATE DATABASE granite_db;
GRANT ALL PRIVILEGES ON DATABASE granite_db TO granite;
```
#### MySQL
```sql
CREATE USER 'granite'@'localhost' IDENTIFIED BY 'password';
CREATE DATABASE granite_db;
GRANT ALL PRIVILEGES ON granite_db.* TO 'granite'@'localhost' WITH GRANT OPTION;
```
4. Export `.env` with `$ source ./export.sh` or `$ source .env`.
5. `$ crystal spec`
================================================
FILE: docker/docker-compose.mysql.yml
================================================
version: '2'
services:
spec:
extends:
file: ../docker-compose.yml
service: spec
environment:
CURRENT_ADAPTER: mysql
depends_on:
- mysql
mysql:
image: mysql:${MYSQL_VERSION}
environment:
MYSQL_ROOT_PASSWORD: pass
MYSQL_DATABASE: test
MYSQL_USER: user
MYSQL_PASSWORD: pass
================================================
FILE: docker/docker-compose.pg.yml
================================================
version: '2'
services:
spec:
extends:
file: ../docker-compose.yml
service: spec
environment:
CURRENT_ADAPTER: pg
depends_on:
- pg
pg:
image: postgres:${PG_VERSION}
environment:
POSTGRES_PASSWORD: pass
================================================
FILE: docker/docker-compose.sqlite.yml
================================================
version: '2'
services:
spec:
extends:
file: ../docker-compose.yml
service: spec
build:
context: ../
environment:
CURRENT_ADAPTER: sqlite
================================================
FILE: docker-compose.yml
================================================
version: '2'
services:
spec:
build: .
command: 'bash -c "cd /app/user && bin/ameba && crystal tool format --check && crystal spec --warnings all"'
working_dir: /app/user
environment:
PG_DATABASE_URL: 'postgres://postgres:pass@pg:5432/postgres'
MYSQL_DATABASE_URL: 'mysql://user:pass@mysql:3306/test'
SQLITE_DATABASE_URL: 'sqlite3:./test.db'
CURRENT_ADAPTER: sqlite
================================================
FILE: docs/callbacks.md
================================================
# Callbacks
Call a specified method on a specific life cycle event.
Here is an example:
```crystal
require "granite/adapter/pg"
class Post < Granite::Base
connection pg
before_save :upcase_title
column id : Int64, primary: true
column title : String
column content : String
timestamps
def upcase_title
if title = @title
@title = title.upcase
end
end
end
```
You can register callbacks for the following events:
## Create
- before_save
- before_create
- **save**
- after_create
- after_save
## Update
- before_save
- before_update
- **save**
- after_update
- after_save
## Destroy
- before_destroy
- **destroy**
- after_destroy
================================================
FILE: docs/crud.md
================================================
# CRUD
## Create
Combination of object creation and insertion into database.
```crystal
Post.create(name: "Granite Rocks!", body: "Check this out.") # Set attributes and call save
Post.create!(name: "Granite Rocks!", body: "Check this out.") # Set attributes and call save!. Will throw an exception when the save failed
```
To create a record without setting the `created_at` & `updated_at` fields, you can pass in `skip_timestamps`.
```crystal
Post.create({name: "Granite Rocks!", body: "Check this out."}, skip_timestamps: true)
```
## Insert
Inserts an already created object into the database.
```crystal
post = Post.new
post.name = "Granite Rocks!"
post.body = "Check this out."
post.save
post = Post.new
post.name = "Granite Rocks!"
post.body = "Check this out."
post.save! # raises when save failed
```
To skip the validation callbacks, pass in `validate: false`:
```crystal
post.save(validate: false)
post.save!(validate: false)
```
You can also pass in `skip_timestamps` to save without changing the `updated_at` field on update:
```crystal
post.save(skip_timestamps: true)
post.save!(skip_timestamps: true)
```
## Read
### find
Finds the record with the given primary key.
```crystal
post = Post.find 1
if post
puts post.name
end
post = Post.find! 1 # raises when no records found
```
### find_by
Finds the record(s) that match the given criteria
```crystal
post = Post.find_by(slug: "example_slug")
if post
puts post.name
end
post = Post.find_by!(slug: "foo") # raises when no records found.
other_post = Post.find_by(slug: "foo", type: "bar") # Also works for multiple arguments.
```
### first
Returns the first record.
```crystal
post = Post.first
if post
puts post.name
end
post = Post.first! # raises when no records exist
```
### reload
Returns the record with the attributes reloaded from the database.
**Note:** this method is only defined when the `Spec` module is present.
```
post = Post.create(name: "Granite Rocks!", body: "Check this out.")
# record gets updated by another process
post.reload # performs another find to fetch the record again
```
### where, order, limit, offset, group_by
See [querying](./querying.md) for more details of using the QueryBuilder.
### all
Returns all records of a model.
```crystal
posts = Post.all
if posts
posts.each do |post|
puts post.name
end
end
```
See [querying](./querying.md#all) for more details on using `all`
## Update
Updates a given record already saved in the database.
```crystal
post = Post.find 1
post.name = "Granite Really Rocks!"
post.save
post = Post.find 1
post.update(name: "Granite Really Rocks!") # Assigns attributes and calls save
post = Post.find 1
post.update!(name: "Granite Really Rocks!") # Assigns attributes and calls save!. Will throw an exception when the save failed
```
To update a record without changing the `updated_at` field, you can pass in `skip_timestamps`:
```crystal
post = Post.find 1
post.update({name: "Granite Really Rocks!"}, skip_timestamps: true)
post.update!({name: "Granite Really Rocks!"}, skip_timestamps: true)
```
## Delete
Delete a specific record.
```crystal
post = Post.find 1
post.destroy if post
puts "deleted" if post.destroyed?
post = Post.find 1
post.destroy! # raises when delete failed
```
Clear all records of a model
```crystal
Post.clear #truncate the table
```
================================================
FILE: docs/imports.md
================================================
# Bulk Insertions
## Import
> **Note:** Imports do not trigger callbacks automatically. See [Running Callbacks](#running-callbacks).
Each model has an `.import` method that will save an array of models in one bulk insert statement.
```Crystal
models = [
Model.new(id: 1, name: "Fred", age: 14),
Model.new(id: 2, name: "Joe", age: 25),
Model.new(id: 3, name: "John", age: 30),
]
Model.import(models)
```
## update_on_duplicate
The `import` method has an optional `update_on_duplicate` + `columns` params that allows you to specify the columns (as an array of strings) that should be updated if primary constraint is violated.
```Crystal
models = [
Model.new(id: 1, name: "Fred", age: 14),
Model.new(id: 2, name: "Joe", age: 25),
Model.new(id: 3, name: "John", age: 30),
]
Model.import(models)
Model.find!(1).name # => Fred
models = [
Model.new(id: 1, name: "George", age: 14),
]
Model.import(models, update_on_duplicate: true, columns: %w(name))
Model.find!(1).name # => George
```
**NOTE: If using PostgreSQL you must have version 9.5+ to have the on_duplicate_key_update feature.**
## ignore_on_duplicate
The `import` method has an optional `ignore_on_duplicate` param, that takes a boolean, which will skip records if the primary constraint is violated.
```Crystal
models = [
Model.new(id: 1, name: "Fred", age: 14),
Model.new(id: 2, name: "Joe", age: 25),
Model.new(id: 3, name: "John", age: 30),
]
Model.import(models)
Model.find!(1).name # => Fred
models = [
Model.new(id: 1, name: "George", age: 14),
]
Model.import(models, ignore_on_duplicate: true)
Model.find!(1).name # => Fred
```
## batch_size
The `import` method has an optional `batch_size` param, that takes an integer. The batch_size determines the number of models to import in each INSERT statement. This defaults to the size of the models array, i.e. only 1 INSERT statement.
```Crystal
models = [
Model.new(id: 1, name: "Fred", age: 14),
Model.new(id: 2, name: "Joe", age: 25),
Model.new(id: 3, name: "John", age: 30),
Model.new(id: 3, name: "Bill", age: 66),
]
Model.import(models, batch_size: 2)
# => First SQL INSERT statement imports Fred and Joe
# => Second SQL INSERT statement imports John and Bill
```
## Running Callbacks
Since the `import` method runs on the class level, callbacks are not triggered automatically, they have to be triggered manually. For example, using the Item class with a UUID primary key:
```Crystal
require "uuid"
class Item < Granite::Base
connection mysql
table items
column item_id : String, primary: true, auto: false
column item_name : String
before_create :generate_uuid
def generate_uuid
@item_id = UUID.random.to_s
end
end
```
```Crystal
items = [
Item.new(item_name: "item1"),
Item.new(item_name: "item2"),
Item.new(item_name: "item3"),
Item.new(item_name: "item4"),
]
# If we did `Item.import(items)` now, it would fail since the item_id wouldn't get set before saving the record, violating the primary key constraint.
# Manually run the callback on each model to generate the item_id.
items.each(&.before_create)
# Each model in the array now has a item_id set, so can be imported.
Item.import(items)
# This can also be used for a single record.
item = Item.new(item_name: "item5")
item.before_create
item.save
```
> **Note:** Manually running your callbacks is mainly aimed at bulk imports. Running them before a normal `.save`, for example, would run your callbacks twice.
================================================
FILE: docs/migrations.md
================================================
# Migrations
## Database Migrations with micrate
If you're using Granite to query your data, you likely want to manage your database schema as well. Migrations are a great way to do that, so let's take a look at [micrate](https://github.com/juanedi/micrate), a project to manage migrations. We'll use it as a dependency instead of a pre-build binary.
### Install
Add micrate your shards.yml
```yaml
dependencies:
micrate:
github: juanedi/micrate
```
Update shards
```sh
$ shards update
```
Create an executable to run the `Micrate::Cli`. For this example, we'll create `bin/micrate` in the root of our project where we're using Granite ORM. This assumes you're exporting the `DATABASE_URL` for your project and an environment variable instead of using a `database.yml`.
```crystal
#! /usr/bin/env crystal
#
# To build a standalone command line client, require the
# driver you wish to use and use `Micrate::Cli`.
#
require "micrate"
require "pg"
Micrate::DB.connection_url = ENV["DATABASE_URL"]
Micrate::Cli.run
```
Make it executable:
```sh
$ chmod +x bin/micrate
```
We should now be able to run micrate commands.
`$ bin/micrate help` => should output help commands.
### Creating a migration
Let's create a `posts` table in our database.
```sh
$ bin/micrate scaffold create_posts
```
This will create a file under `db/migrations`. Let's open it and define our posts schema.
```sql
-- +micrate Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE posts(
id BIGSERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
-- +micrate Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE posts;
```
And now let's run the migration
```sh
$ bin/micrate up
```
You should now have a `posts` table in your database ready to query.
================================================
FILE: docs/models.md
================================================
# Model Usage
## Multiple Connections
It is possible to register multiple connections, for example:
```crystal
Granite::Connections << Granite::Adapter::Mysql.new(name: "legacy_db", url: "LEGACY_DB_URL")
Granite::Connections << Granite::Adapter::Pg.new(name: "new_db", url: "NEW_DB_URL")
class Foo < Granite::Base
connection legacy_db
# model fields
end
class Bar < Granite::Base
connection new_db
# model fields
end
```
In this example, we defined two connections. One to a MySQL database named "legacy_db", and another to a PG database named "new_db". The connection name given in the model maps to the name of a registered connection.
> **NOTE:** How you store/supply each connection's URL is up to you; Granite only cares that it gets registered via `Granite::Connections << adapter_object`.
## timestamps
The `timestamps` macro defines `created_at` and `updated_at` field for you.
```crystal
class Bar < Granite::Base
connection mysql
# Other fields
timestamps
end
```
Would be equivalent to:
```crystal
class Bar < Granite::Base
connection mysql
column created_at : Time?
column updated_at : Time?
end
```
## Primary Keys
Each model is required to have a primary key defined. Use the `column` macro with the `primary: true` option to denote the primary key.
> **NOTE:** Composite primary keys are not yet supported.
```crystal
class Site < Granite::Base
connection mysql
column id : Int64, primary: true
column name : String
end
```
`belongs_to` associations can also be used as a primary key in much the same way.
```crystal
class ChatSettings < Granite::Base
connection mysql
# chat_id would be the primary key
belongs_to chat : Chat, primary: true
end
```
### Custom
The name and type of the primary key can also be changed from the recommended `id : Int64`.
```crystal
class Site < Granite::Base
connection mysql
column custom_id : Int32, primary: true
column name : String
end
```
### Natural Keys
Primary keys are defined as auto incrementing by default. For natural keys, you can set `auto: false` option.
```crystal
class Site < Granite::Base
connection mysql
column custom_id : Int32, primary: true, auto: false
column name : String
end
```
### UUIDs
For databases that utilize UUIDs as the primary key, the type of the primary key can be set to `UUID`. This will generate a secure UUID when the model is saved.
```crystal
class Book < Granite::Base
connection mysql
column isbn : UUID, primary: true
column name : String
end
book = Book.new
book.name = "Moby Dick"
book.isbn # => nil
book.save
book.isbn # => RFC4122 V4 UUID string
```
## Default values
A default value can be defined that will be used if another value is not specified/supplied.
```crystal
class Book < Granite::Base
connection mysql
column id : Int64, primary: true
column name : String = "DefaultBook"
end
book = Book.new
book.name # => "DefaultBook"
```
## Generating Documentation
By default, running `crystal docs` will **not** include Granite methods, constants, and properties. To include these, use the `granite_docs` flag when generating the documentation. E.x. `crystal docs -D granite_docs`.
Doc block comments can be applied above the `column` macro.
```crystal
# If the item is public.
column published : Bool
```
## Annotations
Annotations can be a powerful method of adding property specific features with minimal amounts of code. Since Granite utilizes the `property` keyword for its columns, annotations are able to be applied easily. These can either be `JSON::Field`, `YAML::Field`, or third party annotations.
```crystal
class Foo < Granite::Base
connection mysql
table foos
column id : Int64, primary: true
@[JSON::Field(ignore: true)]
@[Bar::Settings(other_option: 7)]
column password : String
column name : String
column age : Int32
end
```
## Converters
Granite supports custom/special types via converters. Converters will convert the type into something the database can store when saving the model, and will convert the returned database value into that type on read.
Each converter has a `T` generic argument that tells the converter what type should be read from the `DB::ResultSet`. For example, if you wanted to use the `JSON` converter and your underlying database column is `BLOB`, you would use `Bytes`, if it was `TEXT`, you would use `String`.
Currently Granite supports various converters, each with their own supported database column types:
- `Enum(E, T)` - Converts an Enum of type `E` to/from a database column of type `T`. Supported types for `T` are: `Number`, `String`, and `Bytes`.
- `Json(M, T)` - Converters an `Object` of type `M` to/from a database column of type `T.` Supported types for `T` are: `String`, `JSON::Any`, and `Bytes`.
- **NOTE:** `M` must implement `#to_json` and `.from_json` methods.
- `PgNumeric` - Converts a `PG::Numeric` value to a `Float64` on read.
The converter is defined on a per field basis. This example has an `OrderStatus` enum typed field. When saved, the enum value would be converted to a string to be stored in the DB. Then, when read, the string would be used to parse a new instance of `OrderStatus`.
```crystal
enum OrderStatus
Active
Expired
Completed
end
class Order < Granite::Base
connection mysql
table foos
# Other fields
column status : OrderStatus, converter: Granite::Converters::Enum(OrderStatus, String)
end
```
## Serialization
Granite implements [JSON::Serializable](https://crystal-lang.org/api/JSON/Serializable.html) and [YAML::Serializable](https://crystal-lang.org/api/YAML/Serializable.html) by default. As such, models can be serialized to/from JSON/YAML via the `#to_json`/`#to_yaml` and `.from_json`/`.from_yaml` methods.
================================================
FILE: docs/multiple_connections.md
================================================
# Read replica support
In Granite, you can create a connection that has a write/read node. If this is done. Granite will perform write operations on the primary node and read operations on the secondary node. Here is an example:
```crystal
Granite::Connections << {name: "psql", writer: "...", reader: "...", adapter_type: Granite::Adapter::Pg}
```
The first parameter `name` is the name of the connection. When you create a model in Granite, you can specify a connection via the `connection` macro. If I wanted to use the above connection in a model. I would write
```crystal
class User < Granite::Base
connection "psql"
end
```
where the value provided to the `connection` macro is the name of the granite connection you want to use.
The `writer` is a connection string to the database node that has read/write access.
The `reader` is a connection string to the database node that can only be read from.
The final argument is a subclass of `Granite::Adapter::Base`. You're basically telling granite what kind of database adapter to use for this connection. Granite comes with adapters for Postgres, MySQL, and SQLite.
## configuring the connection switch wait period
By default, when you perform a write operation on a Granite model, all read requests switch to using the primary database node. This is to allow the changes done to propogate to the read replicas before using them again.
The default value is `2000` milliseconds. You can change this value like this
```crystal
Granite::Conections.connection_switch_wait_period = 2000 #=> time in milliseconds
```
================================================
FILE: docs/querying.md
================================================
# Querying
The query macro and where clause combine to give you full control over your query.
## Where
Where is using a QueryBuilder that allows you to chain where clauses together to build up a complete query.
```crystal
posts = Post.where(published: true, author_id: User.first!.id)
```
It supports different operators:
```crystal
Post.where(:created_at, :gt, Time.local - 7.days)
```
Supported operators are :eq, :gteq, :lteq, :neq, :gt, :lt, :nlt, :ngt, :ltgt, :in, :nin, :like, :nlike
Alternatively, `#where`, `#and`, and `#or` accept a raw SQL clause, with an optional placeholder (`?` for MySQL/SQLite, `$` for Postgres) to avoid SQL Injection.
```crystal
# Example using Postgres adapter
Post.where(:created_at, :gt, Time.local - 7.days)
.where("LOWER(author_name) = $", name)
.where("tags @> '{"Journal", "Book"}') # PG's array contains operator
```
This is useful for building more sophisticated queries, including queries dependent on database specific features not supported by the operators above. However, **clauses built with this method are not validated.**
## Order
Order is using the QueryBuilder and supports providing an ORDER BY clause:
```crystal
Post.order(:created_at)
```
Direction
```crystal
Post.order(updated_at: :desc)
```
Multiple fields
```crystal
Post.order([:created_at, :title])
```
With direction
```crystal
Post.order(created_at: :desc, title: :asc)
```
## Group By
Group is using the QueryBuilder and supports providing an GROUP BY clause:
```crystal
posts = Post.group_by(:published)
```
Multiple fields
```crystal
Post.group_by([:published, :author_id])
```
## Limit
Limit is using the QueryBuilder and provides the ability to limit the number of tuples returned:
```crystal
Post.limit(50)
```
## Offset
Offset is using the QueryBuilder and provides the ability to offset the results. This is used for pagination:
```crystal
Post.offset(100).limit(50)
```
## All
All is not using the QueryBuilder. It allows you to directly query the database using SQL.
When using the `all` method, the selected fields will match the
fields specified in the model unless the `select` macro was used to customize
the SELECT.
Always pass in parameters to avoid SQL Injection. Use a `?`
in your query as placeholder. Checkout the [Crystal DB Driver](https://github.com/crystal-lang/crystal-db)
for documentation of the drivers.
Here are some examples:
```crystal
posts = Post.all("WHERE name LIKE ?", ["Joe%"])
if posts
posts.each do |post|
puts post.name
end
end
# ORDER BY Example
posts = Post.all("ORDER BY created_at DESC")
# JOIN Example
posts = Post.all("JOIN comments c ON c.post_id = post.id
WHERE c.name = ?
ORDER BY post.created_at DESC",
["Joe"])
```
## Customizing SELECT
The `select_statement` macro allows you to customize the entire query, including the SELECT portion. This shouldn't be necessary in most cases, but allows you to craft more complex (i.e. cross-table) queries if needed:
```crystal
class CustomView < Granite::Base
connection pg
column id : Int64, primary: true
column articlebody : String
column commentbody : String
select_statement <<-SQL
SELECT articles.articlebody, comments.commentbody
FROM articles
JOIN comments
ON comments.articleid = articles.id
SQL
end
```
You can combine this with an argument to `all` or `first` for maximum flexibility:
```crystal
results = CustomView.all("WHERE articles.author = ?", ["Noah"])
```
Note - the column order does matter, and you should match your SELECT query to have the columns in the same order they are in the database.
## Exists?
The `exists?` class method returns `true` if a record exists in the table that matches the provided _id_ or _criteria_, otherwise `false`.
If passed a `Number` or `String`, it will attempt to find a record with that primary key. If passed a `Hash` or `NamedTuple`, it will find the record that matches that criteria, similar to `find_by`.
```crystal
# Assume a model named Post with a title field
post = Post.new(title: "My Post")
post.save
post.id # => 1
Post.exists? 1 # => true
Post.exists? {"id" => 1, :title => "My Post"} # => true
Post.exists? {id: 1, title: "Some Post"} # => false
```
The `exists?` method can also be used with the query builder.
```crystal
Post.where(published: true, author_id: User.first!.id).exists?
Post.where(:created_at, :gt, Time.local - 7.days).exists?
```
================================================
FILE: docs/readme.md
================================================
# Documentation
## Getting Started
### Installation
Add this library to your projects dependencies along with the driver in
your `shard.yml`. This can be used with any framework but was originally
designed to work with the amber framework in mind. This library will work
with Kemal or any other framework as well.
```yaml
dependencies:
granite:
github: amberframework/granite
# Pick your database
mysql:
github: crystal-lang/crystal-mysql
sqlite3:
github: crystal-lang/crystal-sqlite3
pg:
github: will/crystal-pg
```
### Register a Connection
Next you will need to register a connection. This should be one of the first things in your main Crystal file, before Granite is required.
```crystal
Granite::Connections << Granite::Adapter::Mysql.new(name: "mysql", url: "YOUR_DATABASE_URL")
# Rest of code...
```
Supported adapters include: `Mysql, Pg, and Sqlite`.
### Example Model
Here is an example Granite model using the connection registered above.
```crystal
require "granite/adapter/mysql"
class Post < Granite::Base
connection mysql
table posts # Name of the table to use for the model, defaults to class name snake cased
column id : Int64, primary: true # Primary key, defaults to AUTO INCREMENT
column name : String? # Nilable field
column body : String # Not nil field
end
```
## Additional Documentation
[Models](./models.md)
[CRUD](./crud.md)
[Querying](./querying.md)
[Relationships](./relationships.md)
[Validation](./validations.md)
[Callbacks](./callbacks.md)
[Migrations](./migrations.md)
[Imports](./imports.md)
================================================
FILE: docs/relationships.md
================================================
# Relationships
## One to One
For one-to-one relationships, You can use the `has_one` and `belongs_to` in your models.
> **Note:** one-to-one relationship does not support through associations yet.
```crystal
class Team < Granite::Base
has_one :coach
column id : Int64, primary: true
column name : String
end
```
This will add a `coach` and `coach=` instance methods to the team which returns associated coach.
```crystal
class Coach < Granite::Base
table coaches
belongs_to :team
column id : Int64, primary: true
column name : String
end
```
This will add a `team` and `team=` instance method to the coach.
For example:
```crystal
team = Team.find! 1
# has_one side..
puts team.coach
coach = Coach.find! 1
# belongs_to side...
puts coach.team
coach.team = team
coach.save
# or in one-to-one you can also do
team.coach = coach
# coach is the child entity and contians the foreign_key
# so save should called on coach instance
coach.save
```
In this example, you will need to add a `team_id` and index to your coaches table:
```sql
CREATE TABLE coaches (
id BIGSERIAL PRIMARY KEY,
team_id BIGINT,
name VARCHAR,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX team_id_idx ON coaches (team_id);
```
Foreign key is inferred from the class name of the Model which uses `has_one`. In above case `team_id` is assumed to be present in `coaches` table. In case its different you can specify one like this:
```crystal
class Team < Granite::Base
has_one :coach, foreign_key: :custom_id
column id : Int64, primary: true
column name : String
end
class Coach < Granite::Base
belongs_to :team
column id : Int64, primary: true
end
```
The class name inferred from the name but you can specify the class name:
```crystal
class Team < Granite::Base
has_one coach : Coach, foreign_key: :custom_id
# or you can provide the class name as a parameter
has_one :coach, class_name: Coach, foreign_key: :custom_id
column id : Int64, primary: true
column name : String
end
class Coach < Granite::Base
belongs_to team : Team
# provide a custom foreign key
belongs_to team : Team, foreign_key: team_uuid : String
column id : Int64, primary: true
end
```
## One to Many
`belongs_to` and `has_many` macros provide a rails like mapping between Objects.
```crystal
class User < Granite::Base
connection mysql
has_many :post
# pluralization requires providing the class name
has_many posts : Post
# or you can provide class name as a parameter
has_many :posts, class_name: Post
# you can provide a custom foreign key
has_many :posts, class_name: Post, foreign_key: :custom_id
column id : Int64, primary: true
column name : String
column email : String
timestamps
end
```
This will add a `posts` instance method to the user which returns an array of posts.
```crystal
class Post < Granite::Base
connection mysql
table posts
belongs_to :user
# or custom name
belongs_to my_user : User
# or custom foreign key
belongs_to user : User, foreign_key: uuid : String
column id : Int64, primary: true
column title : String
timestamps
end
```
This will add a `user` and `user=` instance method to the post.
For example:
```crystal
user = User.find! 1
user.posts.each do |post|
puts post.title
end
post = Post.find! 1
puts post.user
post.user = user
post.save
```
In this example, you will need to add a `user_id` and index to your posts table:
```sql
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
title VARCHAR,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX user_id_idx ON posts (user_id);
```
## Many to Many
Instead of using a hidden many-to-many table, Granite recommends always creating a model for your join tables. For example, let's say you have many `users` that belong to many `rooms`. We recommend adding a new model called `participants` to represent the many-to-many relationship.
Then you can use the `belongs_to` and `has_many` relationships going both ways.
```crystal
class User < Granite::Base
has_many :participants, class_name: Participant
column id : Int64, primary: true
column name : String
end
class Participant < Granite::Base
table participants
belongs_to :user
belongs_to :room
column id : Int64, primary: true
end
class Room < Granite::Base
table rooms
has_many :participants, class_name: Participant
column id : Int64, primary: true
column name : String
end
```
The Participant class represents the many-to-many relationship between the Users and Rooms.
Here is what the database table would look like:
```sql
CREATE TABLE participants (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
room_id BIGINT,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX user_id_idx ON TABLE participants (user_id);
CREATE INDEX room_id_idx ON TABLE participants (room_id);
```
## has_many through:
As a convenience, we provide a `through:` clause to simplify accessing the many-to-many relationship:
```crystal
class User < Granite::Base
has_many :participants, class_name: Participant
has_many :rooms, class_name: Room, through: :participants
column id : Int64, primary: true
column name : String
end
class Participant < Granite::Base
belongs_to :user
belongs_to :room
column id : Int64, primary: true
end
class Room < Granite::Base
has_many :participants, class_name: Participant
has_many :users, class_name: User, through: :participants
column id : Int64, primary: true
column name : String
end
```
This will allow you to find all the rooms that a user is in:
```crystal
user = User.create(name: "Bob")
room = Room.create(name: "#crystal-lang")
room2 = Room.create(name: "#amber")
Participant.create(user_id: user.id, room_id: room.id)
Participant.create(user_id: user.id, room_id: room2.id)
user.rooms.each do |room|
puts room.name
end
```
And the reverse, all the users in a room:
```crystal
room.users.each do |user|
puts user.name
end
```
================================================
FILE: docs/validations.md
================================================
# Errors
All database errors are added to the `errors` array used by `Granite::Validators` with the symbol `:base`
```crystal
post = Post.new
post.save
post.errors[0].to_s.should eq "ERROR: name cannot be null"
```
## Validations
Validations can be made on models to ensure that given criteria are met.
Models that do not pass the validations will not be saved, and will have the errors added to the model's `errors` array.
For example, asserting that the title on a post is not blank:
```Crystal
class Post < Granite::Base
connection mysql
column id : Int64, primary: true
column title : String
validate :title, "can't be blank" do |post|
!post.title.to_s.blank?
end
end
`
```
## Validation Helpers
A set of common validation macros exist to make validations easier to manage/create.
### Common
- `validate_not_nil :field` - Validates that field should not be nil.
- `validate_is_nil :field` - Validates that field should be nil.
- `validate_is_valid_choice :type, ["allowedType1", "allowedType2"]` - Validates that type should be one of a preset option.
- `validate_exclusion :type, ["notAllowedType1", "notAllowedType2"]` - Validates that type should not be one of a preset option.
- `validate_uniqueness :field` - Validates that the field is unique
### String
- `validate_not_blank :field` - Validates that field should not be blank.
- `validate_is_blank :field` - Validates that field should be blank.
- `validate_min_length :field, 5` - Validates that field should be at least 5 long
- `validate_max_length :field, 20` - Validates that field should be at most 20 long
### String
- `validate_greater_than :field, 0` - Validates that field should be greater than 0.
- `validate_greater_than :field, 0, true` - Validates that field should be greater than or equal to 0.
- `validate_less_than :field, 100` - Validates that field should be less than 100.
- `validate_less_than :field, 100, true` - Validates that field should be less than or equal to 100.
Using the helpers, the previous example could have been written like:
```Crystal
class Post < Granite::Base
connection mysql
column id : Int64, primary: true
column title : String
validate_not_blank :title
end
```
================================================
FILE: export.sh
================================================
#!/bin/bash
if [ -f .env ]; then
while IFS= read -r line; do
export "$line"
done < .env
echo "Environment variables from .env file have been exported."
else
echo "Error: The .env file does not exist."
fi
================================================
FILE: shard.yml
================================================
name: granite
version: 0.23.4
crystal: ">= 1.6.0, < 2.0.0"
authors:
- drujensen <drujensen@gmail.com>
- elorest <isaac@isaacsloan.com>
license: MIT
dependencies:
db:
github: crystal-lang/crystal-db
version: ~> 0.13.1
development_dependencies:
mysql:
github: crystal-lang/crystal-mysql
version: ~> 0.16.0
sqlite3:
github: crystal-lang/crystal-sqlite3
version: ~> 0.21.0
pg:
github: will/crystal-pg
version: ~> 0.29.0
ameba:
github: crystal-ameba/ameba
version: ~> 1.5.0
================================================
FILE: spec/adapter/adapters_spec.cr
================================================
require "../spec_helper"
class Foo < Granite::Base
connection {{env("CURRENT_ADAPTER").id}}
column id : Int64, primary: true
end
class Bar < Granite::Base
column id : Int64, primary: true
end
describe Granite::Connections do
describe "registration" do
it "should allow connections to be be saved and looked up" do
Granite::Connections.registered_connections.size.should eq 2
if connection = Granite::Connections[CURRENT_ADAPTER]
connection[:writer].url.should eq ADAPTER_URL
else
connection.should_not be_falsey
end
case ENV["CURRENT_ADAPTER"]?
when "sqlite"
if connection = Granite::Connections["sqlite_with_replica"]
connection[:writer].url.should eq ENV["SQLITE_DATABASE_URL"]?
connection[:reader].url.should eq ADAPTER_REPLICA_URL
else
connection.should_not be_falsey
end
end
end
it "should disallow multiple connections with the same name" do
Granite::Connections << Granite::Adapter::Pg.new(name: "mysql2", url: "mysql://localhost:3306/test")
expect_raises(Exception, "Adapter with name 'mysql2' has already been registered.") do
Granite::Connections << Granite::Adapter::Pg.new(name: "mysql2", url: "mysql://localhost:3306/test")
end
end
it "should assign the correct connections to a model" do
adapter = Foo.adapter
adapter.name.should eq CURRENT_ADAPTER
adapter.url.should eq ADAPTER_URL
end
it "should use the first registered connection if none are specified" do
adapter = Bar.adapter
adapter.name.should eq CURRENT_ADAPTER
adapter.url.should eq ADAPTER_URL
end
end
end
================================================
FILE: spec/adapter/mysql_spec.cr
================================================
require "../spec_helper"
================================================
FILE: spec/adapter/pg_spec.cr
================================================
require "../spec_helper"
================================================
FILE: spec/adapter/sqlite_spec.cr
================================================
require "../spec_helper"
================================================
FILE: spec/granite/associations/belongs_to_spec.cr
================================================
require "../../spec_helper"
describe "belongs_to" do
it "provides a getter for the foreign entity" do
teacher = Teacher.new
teacher.name = "Test teacher"
teacher.save
klass = Klass.new
klass.name = "Test klass"
klass.teacher_id = teacher.id
klass.save
klass.teacher.id.should eq teacher.id
end
it "provides a setter for the foreign entity" do
teacher = Teacher.new
teacher.name = "Test teacher"
teacher.save
klass = Klass.new
klass.name = "Test klass"
klass.teacher = teacher
klass.save
klass.teacher_id.should eq teacher.id
end
it "supports custom types for the join" do
book = Book.new
book.name = "Screw driver"
book.save
review = BookReview.new
review.book = book
review.body = "Best book ever!"
review.save
review.book.name.should eq "Screw driver"
end
it "supports custom method name" do
author = Person.new
author.name = "John Titor"
author.save
book = Book.new
book.name = "How to Time Traveling"
book.author = author
book.save
book.author.name.should eq "John Titor"
end
it "supports both custom method name and custom types for the join" do
publisher = Company.new
publisher.name = "Amber Framework"
publisher.save
book = Book.new
book.name = "Introduction to Granite"
book.publisher = publisher
book.save
book.publisher.name.should eq "Amber Framework"
end
it "supports json_options" do
publisher = Company.new
publisher.name = "Amber Framework"
publisher.save
book = Book.new
book.name = "Introduction to Granite"
book.publisher = publisher
book.save
book.to_json.should eq %({"id":#{book.id},"name":"Introduction to Granite"})
end
it "supports yaml_options" do
publisher = Company.new
publisher.name = "Amber Framework"
publisher.save
book = Book.new
book.name = "Introduction to Granite"
book.publisher = publisher
book.save
book.to_yaml.should eq %(---\nid: #{book.id}\nname: Introduction to Granite\n)
end
it "provides a method to retrieve parent object that will raise if record is not found" do
book = Book.new
book.name = "Introduction to Granite"
expect_raises Granite::Querying::NotFound, "No Company found where id is NULL" { book.publisher! }
end
it "provides the ability to use a custom primary key" do
courier = Courier.new
courier.courier_id = 139_132_751
courier.issuer_id = 999
service = CourierService.new
service.owner_id = 123_321
service.name = "My Service"
service.save
courier.service = service
courier.save
courier.service!.owner_id.should eq 123_321
end
it "allows a belongs_to association to be a primary key" do
chat = Chat.new
chat.name = "My Awesome Chat"
chat.save
settings = ChatSettings.new
settings.chat = chat
settings.save
settings.chat_id!.should eq chat.id
end
it "provides the ability to define a converter for the foreign key" do
uuid_model = UUIDModel.new
uuid_model.save
uuid_relation = UUIDRelation.new
uuid_relation.uuid_model = uuid_model
uuid_relation.save
uuid_relation.uuid_model_id.should eq uuid_model.uuid
end
end
================================================
FILE: spec/granite/associations/has_many_spec.cr
================================================
require "../../spec_helper"
describe "has_many" do
it "provides a method to retrieve associated objects" do
teacher = Teacher.new
teacher.name = "test teacher"
teacher.save
class1 = Klass.new
class1.name = "Test class 1"
class1.teacher = teacher
class1.save
class2 = Klass.new
class2.name = "Test class 2"
class2.teacher = teacher
class2.save
class3 = Klass.new
class3.name = "Test class 3"
class3.save
teacher.klasses.size.should eq 2
end
context "querying association" do
it "#all" do
teacher = Teacher.new
teacher.name = "test teacher"
teacher.save
klass1 = Klass.new
klass1.name = "Test class X"
klass1.teacher = teacher
klass1.save
klass2 = Klass.new
klass2.name = "Test class X"
klass2.teacher = teacher
klass2.save
klass3 = Klass.new
klass3.name = "Test class with different name"
klass3.teacher = teacher
klass3.save
klasses = teacher.klasses.all("AND klasses.name = ? ORDER BY klasses.id DESC", ["Test class X"])
klasses.map(&.id).should eq [klass2.id, klass1.id]
end
it "#find_by" do
teacher = Teacher.new
teacher.name = "test teacher"
teacher.save
klass1 = Klass.new
klass1.name = "Test class X"
klass1.teacher = teacher
klass1.save
klass2 = Klass.new
klass2.name = "Test class X"
klass2.teacher = teacher
klass2.save
klass3 = Klass.new
klass3.name = "Test class with different name"
klass3.teacher = teacher
klass3.save
klass = teacher.klasses.find_by(name: "Test class with different name")
if klass
klass.id.should eq klass3.id
klass.name.should eq "Test class with different name"
else
klass.should_not be_nil
end
end
it "#find_by!" do
teacher = Teacher.new
teacher.name = "test teacher"
teacher.save
klass1 = Klass.new
klass1.name = "Test class X"
klass1.teacher = teacher
klass1.save
klass2 = Klass.new
klass2.name = "Test class X"
klass2.teacher = teacher
klass2.save
klass3 = Klass.new
klass3.name = "Test class with different name"
klass3.teacher = teacher
klass3.save
klass = teacher.klasses.find_by!(name: "Test class with different name")
klass.id.should eq klass3.id
klass.name.should eq "Test class with different name"
expect_raises(
Granite::Querying::NotFound,
"No #{Klass.name} found where name = not_found"
) do
klass = teacher.klasses.find_by!(name: "not_found")
end
end
it "#find" do
teacher = Teacher.new
teacher.name = "test teacher"
teacher.save
klass1 = Klass.new
klass1.name = "Test class X"
klass1.teacher = teacher
klass1.save
klass2 = Klass.new
klass2.name = "Test class X"
klass2.teacher = teacher
klass2.save
klass3 = Klass.new
klass3.name = "Test class with different name"
klass3.teacher = teacher
klass3.save
klass = teacher.klasses.find(klass1.id)
if klass
klass.id.should eq klass1.id
klass.name.should eq "Test class X"
else
klass.should_not be_nil
end
end
it "#find!" do
teacher = Teacher.new
teacher.name = "test teacher"
teacher.save
klass1 = Klass.new
klass1.name = "Test class X"
klass1.teacher = teacher
klass1.save
klass2 = Klass.new
klass2.name = "Test class X"
klass2.teacher = teacher
klass2.save
klass3 = Klass.new
klass3.name = "Test class with different name"
klass3.teacher = teacher
klass3.save
klass = teacher.klasses.find!(klass1.id)
klass.id.should eq klass1.id
klass.name.should eq "Test class X"
id = klass3.id.as(Int64) + 42
expect_raises(
Granite::Querying::NotFound,
"No #{Klass.name} found where id = #{id}"
) do
teacher.klasses.find!(id)
end
end
it "should respect the current primary key" do
courier1 = Courier.new
courier1.courier_id = 1
courier1.issuer_id = 1
courier1.service_id = 1
courier1.save
courier2 = Courier.new
courier2.courier_id = 2
courier2.issuer_id = 2
courier2.service_id = 1
courier2.save
courier3 = Courier.new
courier3.courier_id = 3
courier3.issuer_id = 3
courier3.service_id = 1
courier3.save
service = CourierService.new
service.name = "My service"
service.owner_id = 1
couriers = service.couriers.to_a
couriers.size.should eq 3
couriers[0].courier_id.should eq courier1.courier_id
couriers[0].issuer_id.should eq courier1.issuer_id
couriers[1].courier_id.should eq courier2.courier_id
couriers[1].issuer_id.should eq courier2.issuer_id
couriers[2].courier_id.should eq courier3.courier_id
couriers[2].issuer_id.should eq courier3.issuer_id
end
end
end
================================================
FILE: spec/granite/associations/has_many_through_spec.cr
================================================
require "../../spec_helper"
describe "has_many, through:" do
it "provides a method to retrieve associated objects through another table" do
student = Student.new
student.name = "test student"
student.save
unrelated_student = Student.new
unrelated_student.name = "other student"
unrelated_student.save
klass1 = Klass.new
klass1.name = "Test class"
klass1.save
klass2 = Klass.new
klass2.name = "Test class"
klass2.save
klass3 = Klass.new
klass3.name = "Test class"
klass3.save
enrollment1 = Enrollment.new
enrollment1.student = student
enrollment1.klass = klass1
enrollment1.save
enrollment2 = Enrollment.new
enrollment2.student = student
enrollment2.klass = klass2
enrollment2.save
enrollment3 = Enrollment.new
enrollment3.klass = klass2
enrollment3.student = unrelated_student
enrollment3.save
student.klasses.compact_map(&.id).sort!.should eq [klass1.id, klass2.id].compact.sort!
klass2.students.compact_map(&.id).sort!.should eq [student.id, unrelated_student.id].compact.sort!
end
context "querying association" do
it "#all" do
student = Student.create(name: "test student")
klass1 = Klass.create(name: "Test class X")
klass2 = Klass.create(name: "Test class X")
klass3 = Klass.create(name: "Test class with different name")
enrollment1 = Enrollment.new
enrollment1.student = student
enrollment1.klass = klass1
enrollment1.save
enrollment2 = Enrollment.new
enrollment2.student = student
enrollment2.klass = klass2
enrollment2.save
enrollment3 = Enrollment.new
enrollment3.klass = klass3
enrollment3.student = student
enrollment3.save
klasses = student.klasses.all("AND klasses.name = ? ORDER BY klasses.id DESC", ["Test class X"])
klasses.map(&.id).should eq [klass2.id, klass1.id]
end
it "#find_by" do
student = Student.create(name: "test student")
klass1 = Klass.create(name: "Test class X")
klass2 = Klass.create(name: "Test class X")
klass3 = Klass.create(name: "Test class with different name")
enrollment1 = Enrollment.new
enrollment1.student = student
enrollment1.klass = klass1
enrollment1.save
enrollment2 = Enrollment.new
enrollment2.student = student
enrollment2.klass = klass2
enrollment2.save
enrollment3 = Enrollment.new
enrollment3.klass = klass3
enrollment3.student = student
enrollment3.save
klass = student.klasses.find_by(name: "Test class with different name")
if klass
klass.id.should eq klass3.id
klass.name.should eq "Test class with different name"
else
klass.should_not be_nil
end
end
it "#find_by!" do
student = Student.create(name: "test student")
klass1 = Klass.create(name: "Test class X")
klass2 = Klass.create(name: "Test class X")
klass3 = Klass.create(name: "Test class with different name")
enrollment1 = Enrollment.new
enrollment1.student = student
enrollment1.klass = klass1
enrollment1.save
enrollment2 = Enrollment.new
enrollment2.student = student
enrollment2.klass = klass2
enrollment2.save
enrollment3 = Enrollment.new
enrollment3.klass = klass3
enrollment3.student = student
enrollment3.save
klass = student.klasses.find_by!(name: "Test class with different name")
klass.id.should eq klass3.id
klass.name.should eq "Test class with different name"
expect_raises(
Granite::Querying::NotFound,
"No #{Klass.name} found where name = not_found"
) do
klass = student.klasses.find_by!(name: "not_found")
end
end
it "#find" do
student = Student.create(name: "test student")
klass1 = Klass.create(name: "Test class X")
klass2 = Klass.create(name: "Test class X")
klass3 = Klass.create(name: "Test class with different name")
enrollment1 = Enrollment.new
enrollment1.student = student
enrollment1.klass = klass1
enrollment1.save
enrollment2 = Enrollment.new
enrollment2.student = student
enrollment2.klass = klass2
enrollment2.save
enrollment3 = Enrollment.new
enrollment3.klass = klass3
enrollment3.student = student
enrollment3.save
klass = student.klasses.find(klass1.id)
if klass
klass.id.should eq klass1.id
klass.name.should eq "Test class X"
else
klass.should_not be_nil
end
end
it "#find!" do
student = Student.create(name: "test student")
klass1 = Klass.create(name: "Test class X")
klass2 = Klass.create(name: "Test class X")
klass3 = Klass.create(name: "Test class with different name")
enrollment1 = Enrollment.new
enrollment1.student = student
enrollment1.klass = klass1
enrollment1.save
enrollment2 = Enrollment.new
enrollment2.student = student
enrollment2.klass = klass2
enrollment2.save
enrollment3 = Enrollment.new
enrollment3.klass = klass3
enrollment3.student = student
enrollment3.save
klass = student.klasses.find!(klass1.id)
klass.id.should eq klass1.id
klass.name.should eq "Test class X"
id = klass3.id.as(Int64) + 42
expect_raises(
Granite::Querying::NotFound,
"No #{Klass.name} found where id = #{id}"
) do
student.klasses.find!(id)
end
end
end
end
================================================
FILE: spec/granite/associations/has_one_spec.cr
================================================
require "../../spec_helper"
describe "has_one" do
before_each do
User.clear
Profile.clear
Courier.clear
Character.clear
end
it "provides a setter to set childrens's foriegn_key from parent" do
profile = Profile.new
profile.name = "Test Profile"
profile.save
user = User.new
user.email = "test@domain.com"
user.save
user.profile = profile
profile.user_id.should eq profile.id
end
it "provides a method to retrieve associated objects" do
profile = Profile.new
profile.name = "Test Profile"
profile.save
user = User.new
user.email = "test@domain.com"
user.save
# profile's foriegn_key is now set, so calling save again
user.profile = profile
profile.save
retrieved_profile = user.profile!
retrieved_profile.id.should eq profile.id
end
it "provides a method to retrieve associated object that will raise if record is not found" do
user = User.new
user.email = "test@domain.com"
user.save!
expect_raises Granite::Querying::NotFound, "No Profile found where user_id = #{user.id}" { user.profile! }
end
it "provides the ability to use a custom primary key" do
courier = Courier.new
courier.courier_id = 139_132_750
courier.issuer_id = 999
character = Character.new
character.character_id = 999
character.name = "Mr Jones"
character.save
courier.issuer = character
courier.save
courier.issuer!.character_id.should eq 999
end
end
================================================
FILE: spec/granite/callbacks/abort_spec.cr
================================================
require "../../spec_helper"
describe "#abort!" do
before_each do
CallbackWithAbort.clear
end
context "when create" do
it "doesn't run other callbacks if abort at before_save" do
cwa = CallbackWithAbort.new(abort_at: "before_save", do_abort: true)
cwa.save
cwa.errors.map(&.to_s).should eq(["Aborted at before_save."])
cwa.history.to_s.strip.should eq("")
CallbackWithAbort.find("before_save").should be_nil
end
it "only runs before_save if abort at before_create" do
cwa = CallbackWithAbort.new(abort_at: "before_create", do_abort: true)
cwa.save
cwa.errors.map(&.to_s).should eq(["Aborted at before_create."])
cwa.history.to_s.strip.should eq <<-RUNS
before_save
RUNS
CallbackWithAbort.find("before_create").should be_nil
end
it "runs before_save, before_create and save successfully if abort at after_create" do
cwa = CallbackWithAbort.new(abort_at: "after_create", do_abort: true)
cwa.save
cwa.errors.map(&.to_s).should eq(["Aborted at after_create."])
cwa.history.to_s.strip.should eq <<-RUNS
before_save
before_create
RUNS
CallbackWithAbort.find("after_create").should be_a(CallbackWithAbort)
end
it "runs before_save, before_create, after_create and save successfully if abort at after_save" do
cwa = CallbackWithAbort.new(abort_at: "after_save", do_abort: true)
cwa.save
cwa.errors.map(&.to_s).should eq(["Aborted at after_save."])
cwa.history.to_s.strip.should eq <<-RUNS
before_save
before_create
after_create
RUNS
CallbackWithAbort.find("after_save").should be_a(CallbackWithAbort)
end
end
context "when update" do
it "doesn't run other callbacks if abort at before_save" do
CallbackWithAbort.new(abort_at: "before_save", do_abort: false).save
cwa = CallbackWithAbort.find!("before_save")
cwa.do_abort = true
cwa.save
cwa.errors.map(&.to_s).should eq(["Aborted at before_save."])
cwa.history.to_s.strip.should eq("")
CallbackWithAbort.find!("before_save").do_abort.should be_false
end
it "only runs before_save if abort at before_update" do
CallbackWithAbort.new(abort_at: "before_update", do_abort: false).save
cwa = CallbackWithAbort.find!("before_update")
cwa.do_abort = true
cwa.save
cwa.errors.map(&.to_s).should eq(["Aborted at before_update."])
cwa.history.to_s.strip.should eq <<-RUNS
before_save
RUNS
CallbackWithAbort.find!("before_update").do_abort.should be_false
end
it "runs before_save, before_update and save successfully if abort at after_update" do
CallbackWithAbort.new(abort_at: "after_update", do_abort: false).save
cwa = CallbackWithAbort.find!("after_update")
cwa.do_abort = true
cwa.save
cwa.errors.map(&.to_s).should eq(["Aborted at after_update."])
cwa.history.to_s.strip.should eq <<-RUNS
before_save
before_update
RUNS
CallbackWithAbort.find!("after_update").do_abort.should be_true
end
it "runs before_save, before_update, after_update and save successfully if abort at after_save" do
CallbackWithAbort.new(abort_at: "after_save", do_abort: false).save
cwa = CallbackWithAbort.find!("after_save")
cwa.do_abort = true
cwa.save
cwa.errors.map(&.to_s).should eq(["Aborted at after_save."])
cwa.history.to_s.strip.should eq <<-RUNS
before_save
before_update
after_update
RUNS
CallbackWithAbort.find!("after_save").do_abort.should be_true
end
end
context "when destroy" do
it "doesn't run other callbacks if abort at before_destroy" do
CallbackWithAbort.new(abort_at: "before_destroy", do_abort: true).save
cwa = CallbackWithAbort.find!("before_destroy")
cwa.destroy
cwa.errors.map(&.to_s).should eq(["Aborted at before_destroy."])
cwa.history.to_s.strip.should eq("")
CallbackWithAbort.find("before_destroy").should be_a(CallbackWithAbort)
end
it "runs before_destroy and destroy successfully if abort at after_destory" do
CallbackWithAbort.new(abort_at: "after_destroy", do_abort: true).save
cwa = CallbackWithAbort.find!("after_destroy")
cwa.destroy
cwa.errors.map(&.to_s).should eq(["Aborted at after_destroy."])
cwa.history.to_s.strip.should eq <<-RUNS
before_destroy
RUNS
CallbackWithAbort.find("after_destroy").should be_nil
end
end
end
================================================
FILE: spec/granite/callbacks/callbacks_spec.cr
================================================
require "../../spec_helper"
describe "(callback feature)" do
describe "#save (new record)" do
it "runs before_save, before_create, after_create, after_save" do
callback = Callback.new(name: "foo")
callback.save
callback.history.to_s.strip.should eq <<-EOF
before_save
before_create
after_create
after_save
EOF
end
end
describe "#save" do
it "runs before_save, before_update, after_update, after_save" do
Callback.new(name: "foo").save
callback = Callback.first!
callback.save
callback.history.to_s.strip.should eq <<-EOF
before_save
before_update
after_update
after_save
EOF
end
end
describe "#destroy" do
it "runs before_destroy, after_destroy" do
Callback.new(name: "foo").save
callback = Callback.first!
callback.destroy
callback.history.to_s.strip.should eq <<-EOF
before_destroy
after_destroy
EOF
end
end
describe "an exception thrown in a hook" do
it "should not get swallowed" do
callback = Callback.new(name: "foo")
# close IO in order to raise IO::Error in callback blocks
callback.history.close
expect_raises(IO::Error, "Closed stream") do
callback.save
end
end
end
describe "manually triggered" do
context "on a single model" do
it "should successfully trigger the callback" do
item = Item.new(item_name: "item1")
item.item_id.should be_nil
item.before_create
item.item_id.should be_a(String)
end
end
context "on an array of models" do
it "should successfully trigger the callback" do
items = [] of Item
items << Item.new(item_name: "item1")
items << Item.new(item_name: "item2")
items << Item.new(item_name: "item3")
items << Item.new(item_name: "item4")
items.all? { |item| item.item_id.nil? }.should be_true
items.each(&.before_create)
items.all? { |item| item.item_id.is_a?(String) }.should be_true
end
end
end
end
================================================
FILE: spec/granite/columns/primary_key_spec.cr
================================================
require "../../spec_helper"
describe "#new" do
it "works when the primary is defined as `auto: true`" do
Parent.new
end
it "works when the primary is defined as `auto: false`" do
Kvs.new
end
end
describe "#new(primary_key: value)" do
it "ignores the value in default" do
Parent.new(id: 1_i64).id.should eq(nil)
end
it "sets the value when the primary is defined as `auto: false`" do
Kvs.new(k: "foo").k.should eq("foo")
Kvs.new(k: "foo", v: "v").k.should eq("foo")
end
end
================================================
FILE: spec/granite/columns/read_attribute_spec.cr
================================================
require "../../spec_helper"
describe "read_attribute" do
# Only PG supports array types
{% if env("CURRENT_ADAPTER") == "pg" %}
it "able to read arrays" do
ArrayModel.new.read_attribute("i32_array").should be_nil
end
{% end %}
end
================================================
FILE: spec/granite/columns/timestamps_spec.cr
================================================
require "../../spec_helper"
# Can run this spec for sqlite after https://www.sqlite.org/draft/releaselog/3_24_0.html is released.
{% if ["pg", "mysql"].includes? env("CURRENT_ADAPTER") %}
describe "timestamps" do
it "should uses UTC for created_at by default" do
parent = Parent.new(name: "parent").tap(&.save)
found_parent = Parent.find!(parent.id)
original_timestamp = parent.created_at!
read_timestamp = found_parent.created_at!
original_timestamp.location.should eq Time::Location::UTC
read_timestamp.location.should eq Time::Location::UTC
end
it "should uses UTC for updated_at by default" do
parent = Parent.new(name: "parent").tap(&.save)
found_parent = Parent.find!(parent.id)
original_timestamp = parent.updated_at!
read_timestamp = found_parent.updated_at!
original_timestamp.location.should eq Time::Location::UTC
read_timestamp.location.should eq Time::Location::UTC
end
it "should uses timezone for created_at" do
Granite.settings.default_timezone = "Asia/Shanghai"
parent = Parent.new(name: "parent").tap(&.save)
found_parent = Parent.find!(parent.id)
original_timestamp = parent.created_at!
read_timestamp = found_parent.created_at!
original_timestamp.location.should eq Time::Location.load("Asia/Shanghai")
read_timestamp.location.should eq Time::Location.load("Asia/Shanghai")
end
it "should uses timezone for updated_at" do
Granite.settings.default_timezone = "Asia/Shanghai"
parent = Parent.new(name: "parent").tap(&.save)
found_parent = Parent.find!(parent.id)
original_timestamp = parent.updated_at!
read_timestamp = found_parent.updated_at!
original_timestamp.location.should eq Time::Location.load("Asia/Shanghai")
read_timestamp.location.should eq Time::Location.load("Asia/Shanghai")
end
it "truncates the subsecond parts of created_at" do
parent = Parent.new(name: "parent").tap(&.save)
found_parent = Parent.find!(parent.id)
original_timestamp = parent.created_at!
read_timestamp = found_parent.created_at!
original_timestamp.to_unix.should eq read_timestamp.to_unix
end
it "truncates the subsecond parts of updated_at" do
parent = Parent.new(name: "parent").tap(&.save)
found_parent = Parent.find!(parent.id)
original_timestamp = parent.updated_at!
read_timestamp = found_parent.updated_at!
original_timestamp.to_unix.should eq read_timestamp.to_unix
end
context "bulk imports" do
it "timestamps are returned correctly with bulk imports" do
to_import = [
Parent.new(name: "ParentImport1"),
Parent.new(name: "ParentImport2"),
Parent.new(name: "ParentImport3"),
]
grandma = Parent.new(name: "grandma").tap(&.save)
found_grandma = Parent.find! grandma.id
Parent.import(to_import)
parents = Parent.all("WHERE name LIKE ?", ["ParentImport%"])
parents.size.should eq 3
parents.each do |parent|
parent.updated_at.not_nil!.location.should eq Time::Location::UTC
parent.created_at.not_nil!.location.should eq Time::Location::UTC
found_grandma.updated_at.not_nil!.to_unix.should eq parent.updated_at.not_nil!.to_unix
found_grandma.created_at.not_nil!.to_unix.should eq parent.created_at.not_nil!.to_unix
end
end
it "created_at and updated_at are correctly handled" do
to_import = [
Parent.new(name: "ParentOne"),
]
Parent.import(to_import)
import_time = Time.utc.at_beginning_of_second
parent1 = Parent.find_by!(name: "ParentOne")
parent1.name.should eq "ParentOne"
parent1.created_at!.should eq import_time
parent1.updated_at!.should eq import_time
to_update = Parent.all("WHERE name = ?", ["ParentOne"])
to_update.each { |parent| parent.name = "ParentOneEdited" }
sleep 1
Parent.import(to_update, update_on_duplicate: true, columns: ["name"])
update_time = Time.utc.at_beginning_of_second
parent1_edited = Parent.find_by!(name: "ParentOneEdited")
parent1_edited.name.should eq "ParentOneEdited"
parent1_edited.created_at!.should be_close(import_time, 1.second)
parent1_edited.updated_at!.should be_close(update_time, 1.second)
end
end
end
{% end %}
================================================
FILE: spec/granite/columns/uuid_spec.cr
================================================
require "../../spec_helper"
describe "UUID creation" do
it "correctly sets a RFC4122 V4 UUID on save" do
item = UUIDModel.new
item.uuid.should be_nil
item.save
item.uuid.should be_a(UUID)
item.uuid!.version.v4?.should be_true
item.uuid!.variant.rfc4122?.should be_true
end
end
================================================
FILE: spec/granite/connection_management_spec.cr
================================================
require "spec"
describe "Granite::Base track time since last write" do
it "should switch to reader db connection after connection_switch_wait_period after write operation" do
ReplicatedChat.connection_switch_wait_period = 250
ReplicatedChat.new(content: "hello world!").save!
sleep 500.milliseconds
current_url = ReplicatedChat.adapter.url
reader_connection = Granite::Connections["#{ENV["CURRENT_ADAPTER"]}_with_replica"]
raise "Reader connection cannot be nil" if reader_connection.nil?
reader_url = reader_connection[:reader].url
current_url.should eq reader_url
end
end
================================================
FILE: spec/granite/converters/converters_spec.cr
================================================
require "../../spec_helper"
describe Granite::Converters do
{% if env("CURRENT_ADAPTER") == "pg" %}
describe "#save" do
it "should handle nil values" do
model = ConverterModel.new
model.save.should be_true
model.id.should be_a Int64
# Enums
model.smallint_enum.should be_nil
model.bigint_enum.should be_nil
model.string_enum.should be_nil
model.enum_enum.should be_nil
model.binary_enum.should be_nil
# Numeric
model.numeric.should be_nil
# JSON
model.string_json.should be_nil
model.string_jsonb.should be_nil
model.binary_json.should be_nil
end
it "should handle actual values" do
obj = MyType.new
model = ConverterModel.new numeric: Math::PI.round(20)
model.binary_json = model.string_jsonb = model.string_json = obj
model.smallint_enum = MyEnum::Zero
model.bigint_enum = MyEnum::One
model.string_enum = MyEnum::Two
model.enum_enum = MyEnum::Three
model.binary_enum = MyEnum::Four
model.save.should be_true
model.id.should be_a Int64
# Enums
model.smallint_enum.should eq MyEnum::Zero
model.bigint_enum.should eq MyEnum::One
model.string_enum.should eq MyEnum::Two
model.enum_enum.should eq MyEnum::Three
model.binary_enum.should eq MyEnum::Four
# Numeric
model.numeric.should eq Math::PI.round(20)
# JSON
model.string_json.should eq obj
model.string_jsonb.should eq obj
model.binary_json.should eq obj
end
end
describe "#read" do
it "should handle nil values" do
model = ConverterModel.new
model.save.should be_true
model.id.should be_a Int64
retrieved_model = ConverterModel.find! model.id
# Enums
retrieved_model.smallint_enum.should be_nil
retrieved_model.bigint_enum.should be_nil
retrieved_model.string_enum.should be_nil
retrieved_model.enum_enum.should be_nil
retrieved_model.binary_enum.should be_nil
# Numeric
retrieved_model.numeric.should be_nil
# JSON
retrieved_model.string_json.should be_nil
retrieved_model.string_jsonb.should be_nil
retrieved_model.binary_json.should be_nil
end
it "should handle actual values" do
obj = MyType.new
model = ConverterModel.new numeric: Math::PI.round(20)
model.binary_json = model.string_jsonb = model.string_json = obj
model.smallint_enum = MyEnum::Zero
model.bigint_enum = MyEnum::One
model.string_enum = MyEnum::Two
model.enum_enum = MyEnum::Three
model.binary_enum = MyEnum::Four
model.save.should be_true
model.id.should be_a Int64
retrieved_model = ConverterModel.find! model.id
# Enum
retrieved_model.smallint_enum.should eq MyEnum::Zero
retrieved_model.bigint_enum.should eq MyEnum::One
retrieved_model.string_enum.should eq MyEnum::Two
retrieved_model.enum_enum.should eq MyEnum::Three
retrieved_model.binary_enum.should eq MyEnum::Four
# Numeric
retrieved_model.numeric.should eq Math::PI.round(20)
# JSON
retrieved_model.string_json.should eq obj
retrieved_model.string_jsonb.should eq obj
retrieved_model.binary_json.should eq obj
end
end
{% elsif env("CURRENT_ADAPTER") == "sqlite" %}
describe "#save" do
it "should handle nil values" do
model = ConverterModel.new
model.save.should be_true
model.id.should be_a Int64
# Enums
model.int_enum.should be_nil
model.string_enum.should be_nil
model.binary_enum.should be_nil
# JSON
model.string_json.should be_nil
model.binary_json.should be_nil
end
it "should handle actual values" do
obj = MyType.new
model = ConverterModel.new
model.binary_json = model.string_json = obj
model.int_enum = MyEnum::Zero
model.string_enum = MyEnum::Two
model.binary_enum = MyEnum::Four
model.save.should be_true
model.id.should be_a Int64
# Enums
model.int_enum.should eq MyEnum::Zero
model.string_enum.should eq MyEnum::Two
model.binary_enum.should eq MyEnum::Four
# JSON
model.string_json.should eq obj
model.binary_json.should eq obj
end
end
describe "#read" do
it "should handle nil values" do
model = ConverterModel.new
model.save.should be_true
model.id.should be_a Int64
retrieved_model = ConverterModel.find! model.id
# Enums
retrieved_model.int_enum.should be_nil
retrieved_model.string_enum.should be_nil
retrieved_model.binary_enum.should be_nil
# JSON
retrieved_model.string_json.should be_nil
retrieved_model.binary_json.should be_nil
end
it "should handle actual values" do
obj = MyType.new
model = ConverterModel.new
model.binary_json = model.string_json = obj
model.int_enum = MyEnum::Zero
model.string_enum = MyEnum::Two
model.binary_enum = MyEnum::Four
model.save.should be_true
model.id.should be_a Int64
retrieved_model = ConverterModel.find! model.id
# Enums
retrieved_model.int_enum.should eq MyEnum::Zero
retrieved_model.string_enum.should eq MyEnum::Two
retrieved_model.binary_enum.should eq MyEnum::Four
# JSON
retrieved_model.string_json.should eq obj
retrieved_model.binary_json.should eq obj
end
end
{% elsif env("CURRENT_ADAPTER") == "mysql" %}
describe "#save" do
it "should handle nil values" do
model = ConverterModel.new
model.save.should be_true
model.id.should be_a Int64
# Enums
model.int_enum.should be_nil
model.string_enum.should be_nil
model.enum_enum.should be_nil
model.binary_enum.should be_nil
# JSON
model.string_json.should be_nil
model.binary_json.should be_nil
end
it "should handle actual values" do
obj = MyType.new
model = ConverterModel.new
model.binary_json = model.string_json = obj
model.int_enum = MyEnum::Zero
model.string_enum = MyEnum::Two
model.enum_enum = MyEnum::Three
model.binary_enum = MyEnum::Four
model.save.should be_true
model.id.should be_a Int64
# Enums
model.int_enum.should eq MyEnum::Zero
model.string_enum.should eq MyEnum::Two
model.enum_enum.should eq MyEnum::Three
model.binary_enum.should eq MyEnum::Four
# JSON
model.string_json.should eq obj
model.binary_json.should eq obj
end
end
describe "#read" do
it "should handle nil values" do
model = ConverterModel.new
model.save.should be_true
model.id.should be_a Int64
retrieved_model = ConverterModel.find! model.id
# Enums
retrieved_model.int_enum.should be_nil
retrieved_model.string_enum.should be_nil
retrieved_model.enum_enum.should be_nil
retrieved_model.binary_enum.should be_nil
# JSON
retrieved_model.string_json.should be_nil
retrieved_model.binary_json.should be_nil
end
it "should handle actual values" do
obj = MyType.new
model = ConverterModel.new
model.binary_json = model.string_json = obj
model.int_enum = MyEnum::Zero
model.string_enum = MyEnum::Two
model.enum_enum = MyEnum::Three
model.binary_enum = MyEnum::Four
model.save.should be_true
model.id.should be_a Int64
retrieved_model = ConverterModel.find! model.id
# Enums
retrieved_model.int_enum.should eq MyEnum::Zero
retrieved_model.string_enum.should eq MyEnum::Two
retrieved_model.enum_enum.should eq MyEnum::Three
retrieved_model.binary_enum.should eq MyEnum::Four
# JSON
retrieved_model.string_json.should eq obj
retrieved_model.binary_json.should eq obj
end
end
{% end %}
end
================================================
FILE: spec/granite/converters/enum_spec.cr
================================================
require "../../spec_helper"
enum TestEnum
Zero
One
Two
Three = 17
end
describe Granite::Converters::Enum do
describe Number do
describe ".to_db" do
it "should convert a Test enum into a Number" do
Granite::Converters::Enum(TestEnum, Int8).to_db(TestEnum::One).should eq 1_i64
Granite::Converters::Enum(TestEnum, Float64).to_db(TestEnum::Two).should eq 2_i64
Granite::Converters::Enum(TestEnum, Int32).to_db(TestEnum::Three).should eq 17_i64
end
end
describe ".from_rs" do
it "should convert the RS value into a Test enum" do
rs = FieldEmitter.new.tap do |e|
e._set_values([0])
end
Granite::Converters::Enum(TestEnum, Int32).from_rs(rs).should eq TestEnum::Zero
end
it "should convert the RS value into a Test enum" do
rs = FieldEmitter.new.tap do |e|
e._set_values([1_i16])
end
Granite::Converters::Enum(TestEnum, Int16).from_rs(rs).should eq TestEnum::One
end
it "should convert the RS value into a Test enum" do
rs = FieldEmitter.new.tap do |e|
e._set_values([17.0])
end
Granite::Converters::Enum(TestEnum, Float64).from_rs(rs).should eq TestEnum::Three
end
end
end
describe String do
describe ".to_db" do
it "should convert a Test enum into a string" do
Granite::Converters::Enum(TestEnum, String).to_db(TestEnum::Two).should eq "Two"
end
end
describe ".from_rs" do
it "should convert the RS value into a Test enum" do
rs = FieldEmitter.new.tap do |e|
e._set_values(["Three"])
end
Granite::Converters::Enum(TestEnum, String).from_rs(rs).should eq TestEnum::Three
end
end
end
describe Bytes do
describe ".to_db" do
it "should convert a Test enum into a string" do
Granite::Converters::Enum(TestEnum, Bytes).to_db(TestEnum::Two).should eq "Two"
end
end
describe ".from_rs" do
it "should convert an Int32 value into a Test enum" do
rs = FieldEmitter.new.tap do |e|
e._set_values([Bytes[90, 101, 114, 111]])
end
Granite::Converters::Enum(TestEnum, Bytes).from_rs(rs).should eq TestEnum::Zero
end
end
end
end
================================================
FILE: spec/granite/converters/json_spec.cr
================================================
require "../../spec_helper"
describe Granite::Converters::Json do
describe String do
describe ".to_db" do
it "should convert an Object into a String" do
Granite::Converters::Json(MyType, String).to_db(MyType.new).should eq MyType.new.to_json
end
end
describe ".from_rs" do
it "should convert the RS value into a MyType" do
rs = FieldEmitter.new.tap do |e|
e._set_values([MyType.new.to_json])
end
Granite::Converters::Json(MyType, String).from_rs(rs).should eq MyType.new
end
end
end
describe String do
describe ".to_db" do
it "should convert an Object into a String" do
Granite::Converters::Json(MyType, JSON::Any).to_db(MyType.new).should eq MyType.new.to_json
end
end
describe ".from_rs" do
it "should convert the RS value into a MyType" do
rs = FieldEmitter.new.tap do |e|
e._set_values([JSON.parse(MyType.new.to_json)])
end
Granite::Converters::Json(MyType, JSON::Any).from_rs(rs).should eq MyType.new
end
end
end
describe Bytes do
describe ".to_db" do
it "should convert an Object into Bytes" do
Granite::Converters::Json(MyType, Bytes).to_db(MyType.new).should eq MyType.new.to_json.to_slice
end
end
describe ".from_rs" do
it "should convert the RS value into a MyType" do
rs = FieldEmitter.new.tap do |e|
e._set_values([
Bytes[123,
34,
110,
97,
109,
101,
34,
58,
34,
74,
105,
109,
34,
44,
34,
97,
103,
101,
34,
58,
49,
50,
125],
])
end
Granite::Converters::Json(MyType, Bytes).from_rs(rs).should eq MyType.new
end
end
end
end
================================================
FILE: spec/granite/converters/pg_numeric_spec.cr
================================================
require "../../spec_helper"
describe Granite::Converters::PgNumeric do
describe ".to_db" do
it "should convert a Float enum into a Float" do
Granite::Converters::PgNumeric.to_db(3.14).should eq 3.14
end
end
describe ".from_rs" do
it "should convert the RS value into a Float64" do
rs = FieldEmitter.new.tap do |e|
e._set_values([PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 3000_i16])])
end
Granite::Converters::PgNumeric.from_rs(rs).should eq 1.3
end
end
end
================================================
FILE: spec/granite/error/error_spec.cr
================================================
require "../../spec_helper"
describe Granite::Error do
it "should convert to json" do
Granite::Error.new("field", "error message").to_json.should eq %({"field":"field","message":"error message"})
end
end
================================================
FILE: spec/granite/exceptions/record_invalid_spec.cr
================================================
require "../../spec_helper"
describe Granite::RecordNotSaved do
it "should have a message" do
parent = Parent.new
parent.save
Granite::RecordNotSaved
.new(Parent.name, parent)
.message
.should eq("Could not process Parent: Name cannot be blank")
end
it "should have a model" do
parent = Parent.new
parent.save
Granite::RecordNotSaved
.new(Parent.name, parent)
.model
.should eq(parent)
end
end
================================================
FILE: spec/granite/exceptions/record_not_destroyed_spec.cr
================================================
require "../../spec_helper"
describe Granite::RecordNotDestroyed do
it "should have a message" do
parent = Parent.new
parent.save
Granite::RecordNotDestroyed
.new(Parent.name, parent)
.message
.should eq("Could not destroy Parent: Name cannot be blank")
end
it "should have a model" do
parent = Parent.new
parent.save
Granite::RecordNotDestroyed
.new(Parent.name, parent)
.model
.should eq(parent)
end
end
================================================
FILE: spec/granite/integrators/find_or_spec.cr
================================================
require "../../spec_helper"
describe "find_or_create_by, find_or_initialize_by" do
it "creates on find_or_create when not found" do
Parent.clear
Parent.find_or_create_by(name: "name")
Parent.first!.name.should eq("name")
Parent.first!.new_record?.should eq(false)
end
it "uses find on find_or_create_by when it exists" do
Parent.clear
Parent.create(name: "name")
Parent.find_or_create_by(name: "name")
Parent.count.should eq(1)
end
it "uses find on find_or_initialize_by when it exists" do
Parent.clear
Parent.create(name: "name")
parent = Parent.find_or_initialize_by(name: "name")
parent.new_record?.should eq(false)
end
it "initializes with find_or_initialize when not found" do
Parent.clear
parent = Parent.find_or_initialize_by(name: "gnome")
parent.new_record?.should eq(true)
end
end
================================================
FILE: spec/granite/migrator/migrator_spec.cr
================================================
require "../../spec_helper"
describe Granite::Migrator do
describe "#drop_sql" do
it "generates correct SQL with #{{{ env("CURRENT_ADAPTER") }}} adapter" do
{% if env("CURRENT_ADAPTER") == "mysql" %}
Review.migrator.drop_sql.should eq "DROP TABLE IF EXISTS `reviews`;"
{% else %}
Review.migrator.drop_sql.should eq "DROP TABLE IF EXISTS \"reviews\";"
{% end %}
end
end
describe "#create_sql" do
it "generates correct SQL with #{{{ env("CURRENT_ADAPTER") }}} adapter" do
{% if env("CURRENT_ADAPTER") == "pg" %}
Review.migrator.create_sql.should eq <<-SQL
CREATE TABLE "reviews"(
"id" BIGSERIAL PRIMARY KEY,
"name" TEXT
,
"downvotes" INT
,
"upvotes" BIGINT
,
"sentiment" REAL
,
"interest" DOUBLE PRECISION
,
"published" BOOL
,
"created_at" TIMESTAMP
) ;\n
SQL
Kvs.migrator.create_sql.should eq <<-SQL
CREATE TABLE "kvs"(
"k" TEXT PRIMARY KEY,
"v" TEXT
) ;\n
SQL
UUIDModel.migrator.create_sql.should eq <<-SQL
CREATE TABLE "uuids"(
"uuid" UUID PRIMARY KEY) ;\n
SQL
Character.migrator.create_sql.should eq <<-SQL
CREATE TABLE "characters"(
"character_id" SERIAL PRIMARY KEY,
"name" TEXT NOT NULL
) ;\n
SQL
# Also check Array types for pg
ArrayModel.migrator.create_sql.should eq <<-SQL
CREATE TABLE "array_model"(
"id" SERIAL PRIMARY KEY,
"str_array" TEXT[]
,
"i16_array" SMALLINT[]
,
"i32_array" INT[]
,
"i64_array" BIGINT[]
,
"f32_array" REAL[]
,
"f64_array" DOUBLE PRECISION[]
,
"bool_array" BOOLEAN[]
) ;\n
SQL
{% elsif env("CURRENT_ADAPTER") == "mysql" %}
Review.migrator.create_sql.should eq <<-SQL
CREATE TABLE `reviews`(
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255)
,
`downvotes` INT
,
`upvotes` BIGINT
,
`sentiment` FLOAT
,
`interest` DOUBLE
,
`published` BOOL
,
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP
) ;\n
SQL
Kvs.migrator.create_sql.should eq <<-SQL
CREATE TABLE `kvs`(
`k` VARCHAR(255) PRIMARY KEY,
`v` VARCHAR(255)
) ;\n
SQL
Character.migrator.create_sql.should eq <<-SQL
CREATE TABLE `characters`(
`character_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL
) ;\n
SQL
UUIDModel.migrator.create_sql.should eq <<-SQL
CREATE TABLE `uuids`(
`uuid` CHAR(36) PRIMARY KEY) ;\n
SQL
{% elsif env("CURRENT_ADAPTER") == "sqlite" %}
Review.migrator.create_sql.should eq <<-SQL
CREATE TABLE "reviews"(
"id" INTEGER NOT NULL PRIMARY KEY,
"name" VARCHAR(255)
,
"downvotes" INTEGER
,
"upvotes" INTEGER
,
"sentiment" FLOAT
,
"interest" REAL
,
"published" BOOL
,
"created_at" VARCHAR
) ;\n
SQL
Kvs.migrator.create_sql.should eq <<-SQL
CREATE TABLE "kvs"(
"k" VARCHAR(255) PRIMARY KEY,
"v" VARCHAR(255)
) ;\n
SQL
Character.migrator.create_sql.should eq <<-SQL
CREATE TABLE "characters"(
"character_id" INTEGER NOT NULL PRIMARY KEY,
"name" VARCHAR(255) NOT NULL
) ;\n
SQL
UUIDModel.migrator.create_sql.should eq <<-SQL
CREATE TABLE "uuids"(
"uuid" CHAR(36) PRIMARY KEY) ;\n
SQL
{% end %}
end
it "supports a manually supplied column type" do
{% if env("CURRENT_ADAPTER") == "pg" %}
ManualColumnType.migrator.create_sql.should eq <<-SQL
CREATE TABLE "manual_column_types"(
"id" BIGSERIAL PRIMARY KEY,
"foo" DECIMAL(12, 10)
) ;\n
SQL
{% elsif env("CURRENT_ADAPTER") == "mysql" %}
ManualColumnType.migrator.create_sql.should eq <<-SQL
CREATE TABLE `manual_column_types`(
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`foo` DECIMAL(12, 10)
) ;\n
SQL
{% elsif env("CURRENT_ADAPTER") == "sqlite" %}
ManualColumnType.migrator.create_sql.should eq <<-SQL
CREATE TABLE "manual_column_types"(
"id" INTEGER NOT NULL PRIMARY KEY,
"foo" DECIMAL(12, 10)
) ;\n
SQL
{% end %}
end
end
end
================================================
FILE: spec/granite/query/assemblers/mysql_spec.cr
================================================
require "../spec_helper"
{% if env("CURRENT_ADAPTER").id == "mysql" %}
describe Granite::Query::Assembler::Mysql(Model) do
context "count" do
it "counts for where/count queries" do
sql = "select count(*) from table where name = ?"
builder.where(name: "bob").count.raw_sql.should match ignore_whitespace sql
end
it "simple counts" do
sql = "select count(*) from table"
builder.count.raw_sql.should match ignore_whitespace sql
end
it "adds group_by fields for where/count queries" do
sql = "select count(*) from table where name = ? group by name"
builder.where(name: "bob").group_by(:name).count.raw_sql.should match ignore_whitespace sql
end
end
context "group_by" do
it "adds group_by for select query" do
sql = "select #{query_fields} from table group by name order by id desc"
builder.group_by(:name).raw_sql.should match ignore_whitespace sql
end
it "adds multiple group_by for select query" do
sql = "select #{query_fields} from table group by name, age order by id desc"
builder.group_by([:name, :age]).raw_sql.should match ignore_whitespace sql
end
it "adds chain of group_by for select query" do
sql = "select #{query_fields} from table group by id, name, age order by id desc"
builder.group_by(:id).group_by([:name, :age]).raw_sql.should match ignore_whitespace sql
end
end
context "where" do
it "properly numbers fields" do
sql = "select #{query_fields} from table where name = ? and age = ? order by id desc"
query = builder.where(name: "bob", age: "23")
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq ["bob", "23"]
end
it "property defines IN query" do
sql = "SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND status IN (?,?) ORDER BY id DESC"
query = builder.where(date_completed: nil, status: ["outstanding", "in_progress"])
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq ["outstanding", "in_progress"]
end
it "property defines IN query with numbers" do
sql = "SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND id IN (1,2) ORDER BY id DESC"
query = builder.where(date_completed: nil, id: [1, 2])
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq [] of Granite::Columns::Type
end
it "property defines IN query with booleans" do
sql = "SELECT #{query_fields} FROM table WHERE published IN (true,false) ORDER BY id DESC"
query = builder.where(published: [true, false])
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq [] of Granite::Columns::Type
end
it "handles raw SQL" do
sql = "select #{query_fields} from table where name = 'bob' and age = ? and color = ? order by id desc"
query = builder.where("name = 'bob'").where("age = ?", 23).where("color = ?", "red")
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq [23, "red"]
end
end
context "order" do
it "uses default sort when no sort is provided" do
builder.raw_sql.should match ignore_whitespace "select #{query_fields} from table order by id desc"
end
it "uses specified sort when provided" do
sql = "select #{query_fields} from table order by id asc"
builder.order(id: :asc).raw_sql.should match ignore_whitespace sql
end
end
context "offset" do
it "adds offset for select query" do
sql = "select #{query_fields} from table order by id desc offset 8"
builder.offset(8).raw_sql.should match ignore_whitespace sql
end
it "adds offset for first query" do
sql = "select #{query_fields} from table order by id desc limit 1 offset 3"
builder.offset(3).assembler.first.raw_sql.should match ignore_whitespace sql
end
end
context "limit" do
it "adds limit for select query" do
sql = "select #{query_fields} from table order by id desc limit 5"
builder.limit(5).raw_sql.should match ignore_whitespace sql
end
end
end
{% end %}
================================================
FILE: spec/granite/query/assemblers/pg_spec.cr
================================================
require "../spec_helper"
{% if env("CURRENT_ADAPTER").id == "pg" %}
describe Granite::Query::Assembler::Pg(Model) do
context "count" do
it "counts for where/count queries" do
sql = "select count(*) from table where name = $1"
builder.where(name: "bob").count.raw_sql.should match ignore_whitespace sql
end
it "simple counts" do
sql = "select count(*) from table"
builder.count.raw_sql.should match ignore_whitespace sql
end
it "adds group_by fields for where/count queries" do
sql = "select count(*) from table where name = $1 group by name"
builder.where(name: "bob").group_by(:name).count.raw_sql.should match ignore_whitespace sql
end
end
context "group_by" do
it "adds group_by for select query" do
sql = "select #{query_fields} from table group by name order by id desc"
builder.group_by(:name).raw_sql.should match ignore_whitespace sql
end
it "adds multiple group_by for select query" do
sql = "select #{query_fields} from table group by name, age order by id desc"
builder.group_by([:name, :age]).raw_sql.should match ignore_whitespace sql
end
it "adds chain of group_by for select query" do
sql = "select #{query_fields} from table group by id, name, age order by id desc"
builder.group_by(:id).group_by([:name, :age]).raw_sql.should match ignore_whitespace sql
end
end
context "where" do
it "properly numbers fields" do
sql = "select #{query_fields} from table where name = $1 and age = $2 order by id desc"
query = builder.where(name: "bob", age: "23")
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq ["bob", "23"]
end
it "property defines IN query" do
sql = "SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND status IN ($1,$2) ORDER BY id DESC"
query = builder.where(date_completed: nil, status: ["outstanding", "in_progress"])
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq ["outstanding", "in_progress"]
end
it "property defines IN query with numbers" do
sql = "SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND id IN (1,2) ORDER BY id DESC"
query = builder.where(date_completed: nil, id: [1, 2])
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq [] of Granite::Columns::Type
end
it "property defines IN query with booleans" do
sql = "SELECT #{query_fields} FROM table WHERE published IN (true,false) ORDER BY id DESC"
query = builder.where(published: [true, false])
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq [] of Granite::Columns::Type
end
it "handles raw SQL" do
sql = "select #{query_fields} from table where name = 'bob' and age = $1 and color = $2 order by id desc"
query = builder.where("name = 'bob'").where("age = $", 23).where("color = $", "red")
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq [23, "red"]
end
end
context "order" do
it "uses default sort when no sort is provided" do
builder.raw_sql.should match ignore_whitespace "select #{query_fields} from table order by id desc"
end
it "uses specified sort when provided" do
sql = "select #{query_fields} from table order by id asc"
builder.order(id: :asc).raw_sql.should match ignore_whitespace sql
end
end
context "offset" do
it "adds offset for select query" do
sql = "select #{query_fields} from table order by id desc offset 8"
builder.offset(8).raw_sql.should match ignore_whitespace sql
end
it "adds offset for first query" do
sql = "select #{query_fields} from table order by id desc limit 1 offset 3"
builder.offset(3).assembler.first.raw_sql.should match ignore_whitespace sql
end
end
context "limit" do
it "adds limit for select query" do
sql = "select #{query_fields} from table order by id desc limit 5"
builder.limit(5).raw_sql.should match ignore_whitespace sql
end
end
end
{% end %}
================================================
FILE: spec/granite/query/assemblers/sqlite_spec.cr
================================================
require "../spec_helper"
{% if env("CURRENT_ADAPTER").id == "sqlite" %}
describe Granite::Query::Assembler::Sqlite(Model) do
context "count" do
it "counts for where/count queries" do
sql = "select count(*) from table where name = ?"
builder.where(name: "bob").count.raw_sql.should match ignore_whitespace sql
end
it "simple counts" do
sql = "select count(*) from table"
builder.count.raw_sql.should match ignore_whitespace sql
end
it "adds group_by fields for where/count queries" do
sql = "select count(*) from table where name = ? group by name"
builder.where(name: "bob").group_by(:name).count.raw_sql.should match ignore_whitespace sql
end
end
context "group_by" do
it "adds group_by for select query" do
sql = "select #{query_fields} from table group by name order by id desc"
builder.group_by(:name).raw_sql.should match ignore_whitespace sql
end
it "adds multiple group_by for select query" do
sql = "select #{query_fields} from table group by name, age order by id desc"
builder.group_by([:name, :age]).raw_sql.should match ignore_whitespace sql
end
it "adds chain of group_by for select query" do
sql = "select #{query_fields} from table group by id, name, age order by id desc"
builder.group_by(:id).group_by([:name, :age]).raw_sql.should match ignore_whitespace sql
end
end
context "where" do
it "properly numbers fields" do
sql = "select #{query_fields} from table where name = ? and age = ? order by id desc"
query = builder.where(name: "bob", age: "23")
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq ["bob", "23"]
end
it "property defines IN query" do
sql = "SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND status IN (?,?) ORDER BY id DESC"
query = builder.where(date_completed: nil, status: ["outstanding", "in_progress"])
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq ["outstanding", "in_progress"]
end
it "property defines IN query with numbers" do
sql = "SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND id IN (1,2) ORDER BY id DESC"
query = builder.where(date_completed: nil, id: [1, 2])
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq [] of Granite::Columns::Type
end
it "property defines IN query with booleans" do
sql = "SELECT #{query_fields} FROM table WHERE published IN (true,false) ORDER BY id DESC"
query = builder.where(published: [true, false])
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq [] of Granite::Columns::Type
end
it "handles raw SQL" do
sql = "select #{query_fields} from table where name = 'bob' and age = ? and color = ? order by id desc"
query = builder.where("name = 'bob'").where("age = ?", 23).where("color = ?", "red")
query.raw_sql.should match ignore_whitespace sql
assembler = query.assembler
assembler.where
assembler.numbered_parameters.should eq [23, "red"]
end
end
context "order" do
it "uses default sort when no sort is provided" do
builder.raw_sql.should match ignore_whitespace "select #{query_fields} from table order by id desc"
end
it "uses specified sort when provided" do
sql = "select #{query_fields} from table order by id asc"
builder.order(id: :asc).raw_sql.should match ignore_whitespace sql
end
end
context "offset" do
it "adds offset for select query" do
sql = "select #{query_fields} from table order by id desc offset 8"
builder.offset(8).raw_sql.should match ignore_whitespace sql
end
it "adds offset for first query" do
sql = "select #{query_fields} from table order by id desc limit 1 offset 3"
builder.offset(3).assembler.first.raw_sql.should match ignore_whitespace sql
end
end
context "limit" do
it "adds limit for select query" do
sql = "select #{query_fields} from table order by id desc limit 5"
builder.limit(5).raw_sql.should match ignore_whitespace sql
end
end
end
{% end %}
================================================
FILE: spec/granite/query/builder_spec.cr
================================================
require "./spec_helper"
describe Granite::Query::Builder(Model) do
it "stores where_fields" do
query = builder.where(name: "bob").where(age: 23)
expected = [{join: :and, field: "name", operator: :eq, value: "bob"}, {join: :and, field: "age", operator: :eq, value: 23}]
query.where_fields.should eq expected
end
it "stores operators with where_fields" do
query = builder.where(:name, :like, "bob*").where(:age, :gt, 23)
expected = [{join: :and, field: "name", operator: :like, value: "bob*"}, {join: :and, field: "age", operator: :gt, value: 23}]
query.where_fields.should eq expected
end
it "stores joins with where_fields" do
query = builder.where(:name, :like, "bob*").or(:age, :gt, 23)
expected = [{join: :and, field: "name", operator: :like, value: "bob*"}, {join: :or, field: "age", operator: :gt, value: 23}]
query.where_fields.should eq expected
end
it "stores order fields" do
query = builder.order(name: :desc).order(age: :asc)
expected = [
{field: "name", direction: Granite::Query::Builder::Sort::Descending},
{field: "age", direction: Granite::Query::Builder::Sort::Ascending},
]
query.order_fields.should eq expected
end
it "maps array to :in" do
query = builder.where(date_completed: nil, status: ["outstanding", "in_progress"])
expected = [
{join: :and, field: "date_completed", operator: :eq, value: nil},
{join: :and, field: "status", operator: :in, value: ["outstanding", "in_progress"]},
]
query.where_fields.should eq expected
end
it "stores limit" do
query = builder.limit(7)
query.limit.should eq 7
end
it "stores offset" do
query = builder.offset(17)
query.offset.should eq 17
end
context "raw SQL builder" do
placeholders = {
Granite::Query::Builder::DbType::Mysql => "?",
Granite::Query::Builder::DbType::Sqlite => "?",
Granite::Query::Builder::DbType::Pg => "$",
}
it "chains where statements" do
placeholder = placeholders[builder.db_type]
query = builder.where("name = #{placeholder}", "bob").where("age = #{placeholder}", 23)
expected = [{join: :and, stmt: "name = #{placeholder}", value: "bob"}, {join: :and, stmt: "age = #{placeholder}", value: 23}]
query.where_fields.should eq expected
end
it "chains and statements" do
placeholder = placeholders[builder.db_type]
query = builder.where("name = #{placeholder}", "bob").and("age = #{placeholder}", 23)
expected = [{join: :and, stmt: "name = #{placeholder}", value: "bob"}, {join: :and, stmt: "age = #{placeholder}", value: 23}]
query.where_fields.should eq expected
end
it "chains or statements" do
placeholder = placeholders[builder.db_type]
query = builder.where("name = #{placeholder}", "bob").or("age = #{placeholder}", 23)
expected = [{join: :and, stmt: "name = #{placeholder}", value: "bob"}, {join: :or, stmt: "age = #{placeholder}", value: 23}]
query.where_fields.should eq expected
end
end
end
================================================
FILE: spec/granite/query/executor_spec.cr
================================================
# default value when a Value query is run
# delegates properly
================================================
FILE: spec/granite/query/spec_helper.cr
================================================
require "spec"
require "db"
require "../../../src/granite/query/builder"
class Model
def self.table_name
"table"
end
def self.fields
["name", "age"]
end
def self.primary_name
"id"
end
end
def query_fields
Model.fields.join ", "
end
def builder
{% if env("CURRENT_ADAPTER").id == "pg" %}
Granite::Query::Builder(Model).new Granite::Query::Builder::DbType::Pg
{% elsif env("CURRENT_ADAPTER").id == "mysql" %}
Granite::Query::Builder(Model).new Granite::Query::Builder::DbType::Mysql
{% else %}
Granite::Query::Builder(Model).new Granite::Query::Builder::DbType::Sqlite
{% end %}
end
def ignore_whitespace(expected : String)
whitespace = "\\s+?"
compiled = expected.split(/\s/).map { |s| Regex.escape s }.join(whitespace)
Regex.new "^\\s*#{compiled}\\s*$", Regex::Options::IGNORE_CASE ^ Regex::Options::MULTILINE
end
================================================
FILE: spec/granite/querying/all_spec.cr
================================================
require "../../spec_helper"
describe "#all" do
it "finds all the records" do
Parent.clear
model_ids = (0...100).map do |i|
Parent.new(name: "model_#{i}").tap(&.save)
end.map(&.id)
all = Parent.all
all.size.should eq model_ids.size
all.compact_map(&.id).sort!.should eq model_ids.compact
end
# TODO Fails under MySQL
# it "finds records with numbered query substition" do
# name = "findable model"
# model = Parent.new(name: name).tap(&.save)
# set = Parent.all("WHERE name = $1", [name])
# set.size.should eq 1
# set.first.name.should eq name
# end
it "finds records with question mark substition" do
name = "findable model"
Parent.new(name: name).save
set = Parent.all("WHERE name = ?", [name])
set.size.should eq 1
set.first.name.should eq name
end
end
================================================
FILE: spec/granite/querying/count_spec.cr
================================================
require "../../spec_helper"
describe "#count" do
it "returns 0 if no result" do
Parent.clear
count = Parent.count
count.should eq 0
end
it "returns a number of the all records for the model" do
count = Parent.count
2.times do |i|
Parent.new(name: "model_#{i}").tap(&.save)
end
(Parent.count - count).should eq 2
end
end
================================================
FILE: spec/granite/querying/exists_spec.cr
================================================
require "../../spec_helper"
describe ".exists?" do
before_each do
Parent.clear
end
describe "when there is a record with that ID" do
describe "with a numeric PK" do
it "should return true" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.exists?(model.id).should be_true
end
end
describe "with a string PK" do
it "should return true" do
Kvs.new(k: "EXISTS_ID").save.should be_true
Kvs.exists?("EXISTS_ID").should be_true
end
end
describe "with a namedtuple of args" do
it "should return true" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.exists?(name: "Some Name", id: model.id).should be_true
end
end
describe "with a hash of args" do
it "should return true" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.exists?({:name => "Some Name", "id" => model.id}).should be_true
end
end
describe "with a nil value" do
it "should return true" do
model = Student.new
model.save.should be_true
Student.exists?(name: nil, id: model.id).should be_true
end
end
end
describe "when there is not a record with that ID" do
describe "with a numeric PK" do
it "should return false" do
Parent.exists?(234567).should be_false
end
end
describe "with a string PK" do
it "should return false" do
Kvs.exists?("SOME_KEY").should be_false
end
end
describe "with a namedtuple of args" do
it "should return false" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.exists?(name: "Some Other Name", id: model.id).should be_false
end
end
describe "with a hash of args" do
it "should return false" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.exists?({:name => "Some Other Name", "id" => model.id}).should be_false
end
end
describe "with a nil value" do
it "should return false" do
model = Student.new(name: "Jim")
model.save.should be_true
Student.exists?(name: nil, id: model.id).should be_false
end
end
end
end
================================================
FILE: spec/granite/querying/find_by_spec.cr
================================================
require "../../spec_helper"
describe "#find_by, #find_by!" do
it "finds an object with a string field" do
Parent.clear
name = "robinson"
model = Parent.new(name: name)
model.save.should be_true
found = Parent.find_by(name: name)
if pa = found
pa.id.should eq model.id
else
pa.should_not be_nil
end
found = Parent.find_by!(name: name)
found.should be_a(Parent)
end
it "works with multiple arguments" do
Review.clear
Review.create(name: "review1", upvotes: 2.to_i64)
Review.create(name: "review2", upvotes: 0.to_i64)
expected = Review.create(name: "review3", upvotes: 10.to_i64)
r = Review.find_by(name: "review3", upvotes: 10)
if r
r.id.should eq expected.id
else
r.should_not be_nil
end
expect_raises(Granite::Querying::NotFound, /No .*Review.* found where name = review1 and upvotes = 20/) do
Review.find_by!(name: "review1", upvotes: 20)
end
end
it "finds an object with nil value" do
Student.clear
model = Student.new
model.save.should be_true
found = Student.find_by(name: nil)
if stu = found
stu.id.should eq model.id
else
stu.should_not be_nil
end
found = Student.find_by!(name: nil)
found.should be_a(Student)
end
it "works with reserved words" do
Parent.clear
value = "robinson"
model = ReservedWord.new
model.all = value
model.save.should be_true
found = ReservedWord.find_by(all: value)
if rw = found
rw.id.should eq model.id
else
rw.should_not be_nil
end
found = ReservedWord.find_by!(all: value)
found.id.should eq model.id
end
it "finds an object when provided a hash" do
Parent.clear
name = "johnson"
model = Parent.new(name: name)
model.save.should be_true
found = Parent.find_by({"name" => name})
if pa = found
pa.id.should eq model.id
else
pa.should_not be_nil
end
found = Parent.find_by!({"name" => name})
found.should be_a(Parent)
end
it "returns nil or raises if no result" do
Parent.clear
found = Parent.find_by(name: "xxx")
found.should be_nil
expect_raises(Granite::Querying::NotFound, /No .*Parent.* found where name = xxx/) do
Parent.find_by!(name: "xxx")
end
end
end
================================================
FILE: spec/granite/querying/find_each_spec.cr
================================================
require "../../spec_helper"
describe "#find_each" do
it "finds all the records" do
Parent.clear
model_ids = (0...100).map do |i|
Parent.new(name: "role_#{i}").tap(&.save)
end.map(&.id)
found_roles = [] of Int64 | Nil
Parent.find_each do |model|
found_roles << model.id
end
found_roles.compact.sort!.should eq model_ids.compact
end
it "doesnt yield when no records are found" do
Parent.clear
Parent.find_each do
fail "did yield"
end
end
it "can start from an offset" do
Parent.clear
created_models = (0...10).map do |i|
Parent.new(name: "model_#{i}").tap(&.save)
end.map(&.id)
# discard the first two models
created_models.shift
created_models.shift
found_models = [] of Int64 | Nil
Parent.find_each(offset: 2) do |model|
found_models << model.id
end
found_models.compact.sort!.should eq created_models.compact
end
it "doesnt obliterate a parameterized query" do
Parent.clear
created_models = (0...10).map do |i|
Parent.new(name: "model_#{i}").tap(&.save)
end.map(&.id)
looking_for_ids = created_models[0...5]
found_models = [] of Int64 | Nil
Parent.find_each("WHERE id IN(#{looking_for_ids.join(",")})") do |model|
found_models << model.id
end
found_models.compact.should eq looking_for_ids
end
end
================================================
FILE: spec/granite/querying/find_in_batches.cr
================================================
require "../../spec_helper"
describe "#find_in_batches" do
it "finds records in batches and yields all the records" do
model_ids = (0...100).map do |i|
Parent.new(name: "model_#{i}").tap(&.save)
end.map(&.id)
found_models = [] of Int32 | Nil
Parent.find_in_batches(batch_size: 10) do |batch|
batch.each { |model| found_models << model.id }
batch.size.should eq 10
end
found_models.compact.sort!.should eq model_ids.compact
end
it "doesnt yield when no records are found" do
Parent.find_in_batches do
fail "find_in_batches did yield but shouldn't have"
end
end
it "errors when batch_size is < 1" do
expect_raises ArgumentError do
Parent.find_in_batches batch_size: 0 do
fail "should have raised"
end
end
end
it "returns a small batch when there arent enough results" do
(0...9).each do |i|
Parent.new(name: "model_#{i}").save
end
Parent.find_in_batches(batch_size: 11) do |batch|
batch.size.should eq 9
end
end
it "can start from an offset other than 0" do
created_models = (0...10).map do |i|
Parent.new(name: "model_#{i}").tap(&.save)
end.map(&.id)
# discard the first two models
created_models.shift
created_models.shift
found_models = [] of Int32 | Nil
Parent.find_in_batches(offset: 2) do |batch|
batch.each do |model|
found_models << model.id
end
end
found_models.compact.sort!.should eq created_models.compact
end
it "doesnt obliterate a parameterized query" do
created_models = (0...10).map do |i|
Parent.new(name: "model_#{i}").tap(&.save)
end.map(&.id)
looking_for_ids = created_models[0...5]
Parent.find_in_batches("WHERE id IN(#{looking_for_ids.join(",")})") do |batch|
batch.compact_map(&.id).should eq looking_for_ids
end
end
end
================================================
FILE: spec/granite/querying/find_spec.cr
================================================
require "../../spec_helper"
describe "#find, #find!" do
it "finds an object by id" do
model = Parent.new
model.name = "Test Comment"
model.save
found = Parent.find model.id
found.should_not be_nil
found && (found.id.should eq model.id)
found = Parent.find! model.id
found.id.should eq model.id
end
it "updates states of new_record and persisted" do
model = Parent.new
model.name = "Test Comment"
model.save
model_id = model.id
model = Parent.find!(model_id)
model.new_record?.should be_false
model.persisted?.should be_true
end
describe "with a custom primary key" do
it "finds the object" do
school = School.new
school.name = "Test School"
school.save
primary_key = school.custom_id
found_school = School.find primary_key
found_school.should_not be_nil
found_school = School.find! primary_key
found_school.should be_a(School)
end
end
describe "with a modulized model" do
it "finds the object" do
county = Nation::County.new
county.name = "Test County"
county.save
primary_key = county.id
found_county = Nation::County.find primary_key
found_county.should_not be_nil
found_county = Nation::County.find! primary_key
found_county.should be_a(Nation::County)
end
end
it "returns nil or raises if no result" do
found = Parent.find 0
found.should be_nil
expect_raises(Granite::Querying::NotFound, /No .*Parent.* found where id = 0/) do
Parent.find! 0
end
end
end
================================================
FILE: spec/granite/querying/first_spec.cr
================================================
require "../../spec_helper"
describe "#first, #first!" do
it "finds the first object" do
Parent.clear
first = Parent.new.tap do |model|
model.name = "Test 1"
model.save
end
Parent.new.tap do |model|
model.name = "Test 2"
model.save
end
found = Parent.first
if pa = found
pa.id.should eq first.id
else
pa.should_not be_nil
end
found = Parent.first!
found.id.should eq first.id
end
it "supports a SQL clause" do
Parent.clear
Parent.new.tap do |model|
model.name = "Test 1"
model.save
end
second = Parent.new.tap do |model|
model.name = "Test 2"
model.save
end
found = Parent.first("ORDER BY id DESC")
if pa = found
found.id.should eq second.id
else
pa.should_not be_nil
end
found = Parent.first!("ORDER BY id DESC")
found.id.should eq second.id
end
it "returns nil or raises if no result" do
Parent.clear
Parent.new.tap do |model|
model.name = "Test 1"
model.save
end
found = Parent.first("WHERE name = 'Test 2'")
found.should be nil
expect_raises(Granite::Querying::NotFound, /No .*Parent.* found with first\(WHERE name = 'Test 2'\)/) do
Parent.first!("WHERE name = 'Test 2'")
end
end
end
================================================
FILE: spec/granite/querying/from_rs_spec.cr
================================================
require "../../spec_helper"
macro build_review_emitter
FieldEmitter.new.tap do |e|
e._set_values(
[
8_i64,
"name",
nil, # downvotes
nil, # upvotes
nil, # sentiment
nil, # interest
true, # published
Time.local, # created_at
]
)
end
end
def method_which_takes_any_model(model : Granite::Base.class)
model.as(Granite::Base).from_rs build_review_emitter
end
describe ".from_rs" do
it "Builds a model from a resultset" do
model = Review.from_rs build_review_emitter
model.class.should eq Review
end
end
================================================
FILE: spec/granite/querying/passthrough_spec.cr
================================================
require "../../spec_helper"
describe "#query" do
it "calls query against the db driver" do
Parent.clear
Parent.query "SELECT name FROM parents" do |rs|
rs.column_name(0).should eq "name"
rs.close
end
end
end
describe "#scalar" do
it "calls scalar against the db driver" do
Parent.clear
Parent.scalar "SELECT count(*) FROM parents" do |total|
total.should eq 0
end
end
end
================================================
FILE: spec/granite/querying/query_builder_spec.cr
================================================
require "../../spec_helper"
describe Granite::Query::BuilderMethods do
describe "#where" do
describe "with array arguments" do
it "correctly queries all rows with a list of id values" do
review1 = Review.create(name: "one")
review2 = Review.create(name: "two")
found = Review.where(id: [review1.id, review2.id]).select
found[0].id.should eq review2.id
found[1].id.should eq review1.id
end
it "correctly queries all rows with a list of id values and names" do
review1 = Review.create(name: "one")
review2 = Review.create(name: "two")
found = Review.where(name: ["one", "two"]).and(id: [review1.id, review2.id]).select
found[0].id.should eq review2.id
found[1].id.should eq review1.id
end
it "correctly queries all rows with a list of id values or names" do
review1 = Review.create(name: "one")
review2 = Review.create(name: "two")
found = Review.where(id: [1001, 1002]).or(name: ["one", "two"]).select
found[0].id.should eq review2.id
found[1].id.should eq review1.id
found = Review.where(name: ["one", "two"]).or(id: [1001, 1002]).select
found[0].id.should eq review2.id
found[1].id.should eq review1.id
end
it "correctly queries with ids fields which doest exists" do
Review.create(name: "one")
Review.create(name: "two")
found = Review.where(id: [1001, 1002]).select
found.size.should eq 0
end
it "correctly queries string fields" do
review1 = Review.create(name: "one")
review2 = Review.create(name: "two")
found = Review.where(name: ["one", "two"]).select
found[0].id.should eq review2.id
found[1].id.should eq review1.id
end
it "correctly queries number fields" do
Review.clear
review1 = Review.create(name: "one", downvotes: 99)
review2 = Review.create(name: "two", downvotes: -4)
found = Review.where(downvotes: [99, -4]).select
found[0].id.should eq review2.id
found[1].id.should eq review1.id
end
# Sqlite doesnt have bool literals
{% if env("CURRENT_ADAPTER") == "sqlite" %}
it "correctly queries bool fields" do
Review.clear
Review.create(name: "one", published: 1)
review2 = Review.create(name: "two", published: 0)
found = Review.where(published: [0]).select
found.size.should eq 1
found[0].id.should eq review2.id
end
{% else %}
it "correctly queries bool fields" do
Review.clear
Review.create(name: "one", published: true)
review2 = Review.create(name: "two", published: false)
found = Review.where(published: [false]).select
found.size.should eq 1
found[0].id.should eq review2.id
end
{% end %}
end
end
describe "#exists?" do
describe "when there is a record with that ID" do
describe "when querying on the PK" do
it "should return true" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.where(id: model.id).exists?.should be_true
end
end
describe "with multiple args" do
it "should return true" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.where(name: "Some Name", id: model.id).exists?.should be_true
end
end
end
describe "when there is not a record with that ID" do
describe "when querying on the PK" do
it "should return false" do
Parent.where(id: 234567).exists?.should be_false
end
end
describe "with multiple args" do
it "should return false" do
model = Parent.new(name: "Some Name")
model.save.should be_true
Parent.where(name: "Some Other Name", id: model.id).exists?.should be_false
end
end
end
end
end
================================================
FILE: spec/granite/querying/reload_spec.cr
================================================
require "../../spec_helper"
describe "#reload" do
before_each do
Parent.clear
end
it "reloads the record from the database" do
parent = Parent.create(name: "Parent")
Parent.find!(parent.id).update(name: "Other")
parent.reload.name.should eq "Other"
end
it "raises an error if the record no longer exists" do
parent = Parent.create(name: "Parent")
parent.destroy
expect_raises(Granite::Querying::NotFound) do
parent.reload
end
end
end
================================================
FILE: spec/granite/select/select_spec.cr
================================================
require "../../spec_helper"
describe "custom select" do
before_each do
Article.clear
Comment.clear
EventCon.clear
end
it "generates custom SQL with the query macro" do
ArticleViewModel.select.should eq "SELECT articles.id, articles.articlebody, comments.commentbody FROM articles JOIN comments ON comments.articleid = articles.id"
end
it "uses custom SQL to populate a view model - #all" do
first = Article.new.tap do |model|
model.articlebody = "The Article Body"
model.save!
end
Comment.new.tap do |model|
model.commentbody = "The Comment Body"
model.articleid = first.id
model.save!
end
viewmodel = ArticleViewModel.all
viewmodel.first.articlebody.should eq "The Article Body"
viewmodel.first.commentbody.should eq "The Comment Body"
end
it "allow only selecting specific columns" do
EventCon.create(con_name: "Con0", event_name: "Event0")
EventCon.create(con_name: "Con1", event_name: "Event1")
EventCon.create(con_name: "Con2", event_name: "Event2")
EventCon.all.each_with_index do |env, idx|
env.id.should be_nil
env.con_name.should eq "Con#{idx}"
env.event_name.should be_nil
end
end
# TODO: `find` on this ViewModel fails because "id" is ambiguous in a complex SELECT.
# it "uses custom SQL to populate a view model - #find" do
# first = Article.new.tap do |model|
# model.articlebody = "The Article Body"
# model.save
# end
# second = Comment.new.tap do |model|
# model.commentbody = "The Comment Body"
# model.articleid = first.id
# model.save
# end
# viewmodel = ArticleViewModel.find!(first.id)
# viewmodel.articlebody.should eq "The Article Body"
# viewmodel.commentbody.should eq "The Comment Body"
# end
end
================================================
FILE: spec/granite/table/table_spec.cr
================================================
require "../../spec_helper"
describe Granite::Table do
describe ".table_name" do
it "sets the table name to name specified" do
CustomSongThread.table_name.should eq "custom_table_name"
end
it "sets the table name based on class name if not specified" do
SongThread.table_name.should eq "song_thread"
end
it "strips the namespace when defining the default table now" do
MyApp::Namespace::Model.table_name.should eq "model"
end
end
describe ".primary_name" do
it "sets the primary key name to name specified" do
CustomSongThread.primary_name.should eq "custom_primary_key"
end
it "sets the primary key name to id if not specified" do
SongThread.primary_name.should eq "id"
end
end
end
================================================
FILE: spec/granite/transactions/create_spec.cr
================================================
require "../../spec_helper"
describe "#create" do
it "creates a new object" do
parent = Parent.create(name: "Test Parent")
parent.persisted?.should be_true
parent.name.should eq("Test Parent")
end
it "does not create an invalid object" do
parent = Parent.create(name: "")
parent.persisted?.should be_false
end
describe "with a custom primary key" do
it "creates a new object" do
school = School.create(name: "Test School")
school.persisted?.should be_true
school.name.should eq("Test School")
end
end
describe "with a modulized model" do
it "creates a new object" do
county = Nation::County.create(name: "Test School")
county.persisted?.should be_true
county.name.should eq("Test School")
end
end
describe "using a reserved word as a column name" do
it "creates a new object" do
reserved_word = ReservedWord.create(all: "foo")
reserved_word.errors.empty?.should be_true
reserved_word.all.should eq("foo")
end
end
context "when skip_timestamps is true" do
it "does not update the created_at & updated_at fields" do
parent = Parent.create({name: "new parent"}, skip_timestamps: true)
Parent.find!(parent.id).created_at.should be_nil
Parent.find!(parent.id).updated_at.should be_nil
end
end
end
describe "#create!" do
it "creates a new object" do
parent = Parent.create!(name: "Test Parent")
parent.persisted?.should be_true
parent.name.should eq("Test Parent")
end
it "does not save but raise an exception" do
expect_raises(Granite::RecordNotSaved, "Parent") do
Parent.create!(name: "")
end
end
context "when skip_timestamps is true" do
it "does not update the created_at & updated_at fields" do
parent = Parent.create!({name: "new parent"}, skip_timestamps: true)
Parent.find!(parent.id).created_at.should be_nil
Parent.find!(parent.id).updated_at.should be_nil
end
end
end
================================================
FILE: spec/granite/transactions/destroy_spec.cr
================================================
require "../../spec_helper"
describe "#destroy" do
it "destroys an object" do
parent = Parent.new
parent.name = "Test Parent"
parent.save
id = parent.id
parent.destroy
found = Parent.find id
found.should be_nil
end
it "updates states of destroyed and persisted" do
parent = Parent.new
parent.destroyed?.should be_false
parent.persisted?.should be_false
parent.name = "Test Parent"
parent.save
parent.destroyed?.should be_false
parent.persisted?.should be_true
parent.destroy
parent.destroyed?.should be_true
parent.persisted?.should be_false
end
describe "with a custom primary key" do
it "destroys an object" do
school = School.new
school.name = "Test School"
school.save
primary_key = school.custom_id
school.destroy
found_school = School.find primary_key
found_school.should be_nil
end
end
describe "with a modulized model" do
it "destroys an object" do
county = Nation::County.new
county.name = "Test County"
county.save
primary_key = county.id
county.destroy
found_county = Nation::County.find primary_key
found_county.should be_nil
end
end
end
describe "#destroy!" do
it "destroys an object" do
parent = Parent.new
parent.name = "Test Parent"
parent.save!
id = parent.id
parent.destroy
found = Parent.find id
found.should be_nil
end
it "does not destroy but raise an exception" do
callback_with_abort = CallbackWithAbort.new
callback_with_abort.name = "DestroyRaisesException"
callback_with_abort.abort_at = "temp"
callback_with_abort.do_abort = false
callback_with_abort.save!
callback_with_abort.abort_at = "before_destroy"
callback_with_abort.do_abort = true
expect_raises(Granite::RecordNotDestroyed, "CallbackWithAbort") do
callback_with_abort.destroy!
end
CallbackWithAbort.find_by(name: callback_with_abort.name).should_not be_nil
end
end
================================================
FILE: spec/granite/transactions/import_spec.cr
================================================
require "../../spec_helper"
describe "#import" do
describe "using the defualt primary key" do
context "with an AUTO INCREMENT PK" do
it "should import 3 new objects" do
Parent.clear
to_import = [
Parent.new(name: "ImportParent1"),
Parent.new(name: "ImportParent2"),
Parent.new(name: "ImportParent3"),
]
Parent.import(to_import)
Parent.all("WHERE name LIKE ?", ["ImportParent%"]).size.should eq 3
end
it "should work with batch_size" do
to_import = [
Book.new(name: "ImportBatchBook1"),
Book.new(name: "ImportBatchBook2"),
Book.new(name: "ImportBatchBook3"),
Book.new(name: "ImportBatchBook4"),
]
Book.import(to_import, batch_size: 2)
Book.all("WHERE name LIKE ?", ["ImportBatch%"]).size.should eq 4
end
it "should be able to update existing records" do
to_import = [
Review.new(name: "ImportReview1", published: false, upvotes: 0.to_i64),
Review.new(name: "ImportReview2", published: false, upvotes: 0.to_i64),
Review.new(name: "ImportReview3", published: false, upvotes: 0.to_i64),
Review.new(name: "ImportReview4", published: false, upvotes: 0.to_i64),
]
Review.import(to_import)
reviews = Review.all("WHERE name LIKE ?", ["ImportReview%"])
reviews.size.should eq 4
reviews.none?(&.published).should be_true
reviews.all? { |r| r.upvotes == 0 }.should be_true
reviews.each { |r| r.published = true; r.upvotes = 1.to_i64 }
Review.import(reviews, update_on_duplicate: true, columns: ["published", "upvotes"])
reviews = Review.all("WHERE name LIKE ?", ["ImportReview%"])
reviews.size.should eq 4
reviews.all?(&.published).should be_true
reviews.all? { |r| r.upvotes == 1 }.should be_true
end
end
context "with non AUTO INCREMENT PK" do
it "should work with on_duplicate_key_update" do
to_import = [
NonAutoDefaultPK.new(id: 1.to_i64, name: "NonAutoDefaultPK1"),
NonAutoDefaultPK.new(id: 2.to_i64, name: "NonAutoDefaultPK2"),
NonAutoDefaultPK.new(id: 3.to_i64, name: "NonAutoDefaultPK3"),
]
NonAutoDefaultPK.import(to_import)
to_import = [
NonAutoDefaultPK.new(id: 3.to_i64, name: "NonAutoDefaultPK3"),
]
NonAutoDefaultPK.import(to_import, update_on_duplicate: true, columns: ["name"])
record = NonAutoDefaultPK.find! 3.to_i64
record.name.should eq "NonAutoDefaultPK3"
record.id.should eq 3.to_i64
end
it "should work with on_duplicate_key_ignore" do
to_import = [
NonAutoDefaultPK.new(id: 4.to_i64, name: "NonAutoDefaultPK4"),
NonAutoDefaultPK.new(id: 5.to_i64, name: "NonAutoDefaultPK5"),
NonAutoDefaultPK.new(id: 6.to_i64, name: "NonAutoDefaultPK6"),
]
NonAutoDefaultPK.import(to_import)
to_import = [
NonAutoDefaultPK.new(id: 6.to_i64, name: "NonAutoDefaultPK6"),
]
NonAutoDefaultPK.import(to_import, ignore_on_duplicate: true)
record = NonAutoDefaultPK.find! 6.to_i64
record.name.should eq "NonAutoDefaultPK6"
record.id.should eq 6.to_i64
end
end
end
describe "using a custom primary key" do
context "with an AUTO INCREMENT PK" do
it "should import 3 new objects" do
to_import = [
School.new(name: "ImportBasicSchool1"),
School.new(name: "ImportBasicSchool2"),
School.new(name: "ImportBasicSchool3"),
]
School.import(to_import)
School.all("WHERE name LIKE ?", ["ImportBasicSchool%"]).size.should eq 3
end
it "should work with batch_size" do
to_import = [
School.new(name: "ImportBatchSchool1"),
School.new(name: "ImportBatchSchool2"),
School.new(name: "ImportBatchSchool3"),
School.new(name: "ImportBatchSchool4"),
]
School.import(to_import, batch_size: 2)
School.all("WHERE name LIKE ?", ["ImportBatchSchool%"]).size.should eq 4
end
it "should be able to update existing records" do
to_import = [
School.new(name: "ImportExistingSchool"),
School.new(name: "ImportExistingSchool"),
School.new(name: "ImportExistingSchool"),
School.new(name: "ImportExistingSchool"),
]
School.import(to_import)
schools = School.all("WHERE name = ?", ["ImportExistingSchool"])
schools.size.should eq 4
schools.all? { |s| s.name == "ImportExistingSchool" }.should be_true
schools.each(&.name=("ImportExistingSchoolEdited"))
School.import(schools, update_on_duplicate: true, columns: ["name"])
schools = School.all("WHERE name LIKE ?", ["ImportExistingSchool%"])
schools.size.should eq 4
schools.all? { |s| s.name == "ImportExistingSchoolEdited" }.should be_true
end
end
context "with non AUTO INCREMENT PK" do
it "should work with on_duplicate_key_update" do
to_import = [
NonAutoCustomPK.new(custom_id: 1.to_i64, name: "NonAutoCustomPK1"),
NonAutoCustomPK.new(custom_id: 2.to_i64.to_i64, name: "NonAutoCustomPK2"),
NonAutoCustomPK.new(custom_id: 3.to_i64, name: "NonAutoCustomPK3"),
]
NonAutoCustomPK.import(to_import)
to_import = [
NonAutoCustomPK.new(custom_id: 3.to_i64, name: "NonAutoCustomPK3"),
]
NonAutoCustomPK.import(to_import, update_on_duplicate: true, columns: ["name"])
record = NonAutoCustomPK.find! 3.to_i64
record.name.should eq "NonAutoCustomPK3"
record.custom_id.should eq 3.to_i64
end
it "should work with on_duplicate_key_ignore" do
to_import = [
NonAutoCustomPK.new(custom_id: 4.to_i64, name: "NonAutoCustomPK4"),
NonAutoCustomPK.new(custom_id: 5.to_i64, name: "NonAutoCustomPK5"),
NonAutoCustomPK.new(custom_id: 6.to_i64, name: "NonAutoCustomPK6"),
]
NonAutoCustomPK.import(to_import)
to_import = [
NonAutoCustomPK.new(custom_id: 6.to_i64, name: "NonAutoCustomPK6"),
]
NonAutoCustomPK.import(to_import, ignore_on_duplicate: true)
record = NonAutoCustomPK.find! 6.to_i64
record.name.should eq "NonAutoCustomPK6"
record.custom_id.should eq 6.to_i64
end
end
end
end
================================================
FILE: spec/granite/transactions/save_natural_key_spec.cr
================================================
require "../../spec_helper"
describe "(Natural Key) #save" do
it "fails when a primary key is not set" do
kv = Kvs.new
kv.save.should be_false
kv.errors.first.message.should eq "Primary key('k') cannot be null"
end
it "creates a new object when a primary key is given" do
kv = Kvs.new
kv.k = "foo"
kv.save.should be_true
kv = Kvs.find!("foo")
kv.k.should eq("foo")
end
it "updates an existing object" do
kv = Kvs.new
kv.k = "foo2"
kv.v = "1"
kv.save.should be_true
kv.v = "2"
kv.save.should be_true
kv.k.should eq("foo2")
kv.v.should eq("2")
end
end
describe "(Natural Key) usecases" do
it "CRUD" do
Kvs.clear
# # Create
port = Kvs.new(k: "mysql_port", v: "3306")
port.new_record?.should be_true
port.save.should be_true
port.v.should eq("3306")
Kvs.count.should eq(1)
# # Read
port = Kvs.find!("mysql_port")
port.v.should eq("3306")
port.new_record?.should be_false
# # Update
port.v = "3307"
port.new_record?.should be_false
port.save.should be_true
port.v.should eq("3307")
Kvs.count.should eq(1)
# # Delete
port.destroy.should be_true
Kvs.count.should eq(0)
end
end
================================================
FILE: spec/granite/transactions/save_spec.cr
================================================
require "../../spec_helper"
describe "#save" do
it "creates a new object" do
parent = Parent.new
parent.name = "Test Parent"
parent.save
parent.persisted?.should be_true
end
it "does not create an invalid object" do
parent = Parent.new
parent.name = ""
parent.save
parent.persisted?.should be_false
end
it "create an invalid object with validation disabled" do
parent = Parent.new
parent.name = ""
parent.save(validate: false)
parent.persisted?.should be_true
end
it "does not create an invalid object with validation explicitly enabled" do
parent = Parent.new
parent.name = ""
parent.save(validate: true)
parent.persisted?.should be_false
end
it "does not save a model with type conversion errors" do
model = Comment.new(articleid: "foo")
model.errors.size.should eq 1
model.save.should be_false
end
it "does not update timestamps is skip_timestamps is true" do
time = Time.utc(2023, 9, 1)
parent = Parent.new
parent.name = "Test Parent"
parent.created_at = time
parent.updated_at = time
parent.save(skip_timestamps: true)
parent.created_at.should eq time
parent.updated_at.should eq time
end
it "updates an existing object" do
Parent.clear
parent = Parent.new
parent.name = "Test Parent"
parent.save
parent.name = "Test Parent 2"
parent.save
parents = Parent.all
parents.size.should eq 1
found = Parent.first!
found.name.should eq parent.name
end
it "does not update an invalid object" do
parent = Parent.new
parent.name = "Test Parent"
parent.save
parent.name = ""
parent.save
parent = Parent.find! parent.id
parent.name.should eq "Test Parent"
end
it "update an invalid object with validation disabled" do
Parent.clear
parent = Parent.new
parent.name = "Test Parent"
parent.save
parent.name = ""
parent.save(validate: false)
parents = Parent.all
parents.size.should eq 1
found = Parent.first!
found.name.should eq parent.name
end
it "does not update an invalid object with validation explicitly enabled" do
parent = Parent.new
parent.name = "Test Parent"
parent.save
parent.name = ""
parent.save(validate: true)
parent = Parent.find! parent.id
parent.name.should eq "Test Parent"
end
it "does not update when the conflicted primary key is given to the new record" do
parent1 = Parent.new
parent1.name = "Test Parent"
parent1.save.should be_true
parent2 = Parent.new
parent2.id = parent1.id
parent2.name = "Test Parent2"
parent2.save.should be_false
end
describe "with a custom primary key" do
it "creates a new object" do
school = School.new
school.name = "Test School"
school.save
school.custom_id.should_not be_nil
end
it "updates an existing object" do
old_name = "Test School 1"
new_name = "Test School 2"
school = School.new
school.name = old_name
school.save
primary_key = school.custom_id
school.name = new_name
school.save
found_school = School.find! primary_key
found_school.custom_id.should eq primary_key
found_school.name.should eq new_name
end
it "updates states of new_record and persisted" do
parent = Parent.new
parent.new_record?.should be_true
parent.persisted?.should be_false
parent.name = "Test Parent"
parent.save
parent.new_record?.should be_false
parent.persisted?.should be_true
end
end
describe "with a modulized model" do
it "creates a new object" do
county = Nation::County.new
county.name = "Test School"
county.save
county.persisted?.should be_true
end
it "updates an existing object" do
old_name = "Test County 1"
new_name = "Test County 2"
county = Nation::County.new
county.name = old_name
county.save
primary_key = county.id
county.name = new_name
county.save
found_county = Nation::County.find! primary_key
found_county.name.should eq new_name
end
end
describe "using a reserved word as a column name" do
# `all` is a reserved word in almost RDB like MySQL, PostgreSQL
it "creates and updates" do
reserved_word = ReservedWord.new
reserved_word.all = "foo"
reserved_word.save
reserved_word.errors.empty?.should be_true
reserved_word.all = "bar"
reserved_word.save
reserved_word.errors.empty?.should be_true
reserved_word.all.should eq("bar")
end
end
end
describe "#save!" do
it "creates a new object" do
parent = Parent.new
parent.name = "Test Parent"
parent.save!
parent.persisted?.should be_true
end
it "does not create but raise an exception" do
parent = Parent.new
expect_raises(Granite::RecordNotSaved, "Parent") do
parent.save!
end
end
end
================================================
FILE: spec/granite/transactions/touch_spec.cr
================================================
require "../../spec_helper"
describe "#touch" do
it "should raise on new record" do
expect_raises Exception, "Cannot touch on a new record object" { TimeTest.new.touch }
end
it "should raise on non existent field" do
expect_raises Exception, "Field 'foo' does not exist on type 'TimeTest'." do
model = TimeTest.create(name: "foo")
model.touch(:foo)
end
end
it "should raise on non `Time` field" do
expect_raises Exception, "TimeTest.name cannot be touched. It is not of type `Time`." do
model = TimeTest.create(name: "foo")
model.touch(:name)
end
end
it "updates updated_at on an object" do
old_time = Time.utc.at_beginning_of_second
object = TimeTest.create(test: old_time)
sleep 3
new_time = Time.utc.at_beginning_of_second
object.touch
object.updated_at.should eq new_time
object.test.should eq old_time
object.created_at.should eq old_time
end
it "updates updated_at + custom fields on an object" do
old_time = Time.utc.at_beginning_of_second
object = TimeTest.create(test: old_time)
sleep 3
new_time = Time.utc.at_beginning_of_second
object.touch("test")
object.updated_at.should eq new_time
object.test.should eq new_time
object.created_at.should eq old_time
end
end
================================================
FILE: spec/granite/transactions/update_spec.cr
================================================
require "../../spec_helper"
describe "#update" do
it "updates an object" do
parent = Parent.new(name: "New Parent")
parent.save!
parent.update(name: "Other parent").should be_true
parent.name.should eq "Other parent"
Parent.find!(parent.id).name.should eq "Other parent"
end
it "allows setting a value to nil" do
model = Teacher.create!(name: "New Parent")
model.update(name: nil)
model.name.should be_nil
Teacher.find!(model.id).name.should be_nil
end
it "does not update an invalid object" do
parent = Parent.new(name: "New Parent")
parent.save!
parent.update(name: "").should be_false
parent.name.should eq ""
Parent.find!(parent.id).name.should eq "New Parent"
end
context "when created_at is nil" do
it "does not update created_at" do
parent = Parent.new(name: "New Parent")
parent.save!
created_at = parent.created_at!.at_beginning_of_second
# Simulating instantiating a new object with same ID
new_parent = Parent.new(name: "New New Parent")
new_parent.id = parent.id
new_parent.new_record = false
new_parent.updated_at = parent.updated_at
new_parent.save!
saved_parent = Parent.find!(parent.id)
saved_parent.name.should eq "New New Parent"
saved_parent.created_at.should eq created_at
saved_parent.updated_at.should eq Time.utc.at_beginning_of_second
end
end
context "when skip_timestamps is true" do
it "does not update the updated_at field" do
time = Time.utc(2023, 9, 1)
parent = Parent.create(name: "New Parent")
parent.updated_at = time
parent.update({name: "Other Parent"}, skip_timestamps: true)
Parent.find!(parent.id).updated_at.should eq time
end
end
end
describe "#update!" do
it "updates an object" do
parent = Parent.new(name: "New Parent")
parent.save!
parent.update!(name: "Other parent")
parent.name.should eq "Other parent"
Parent.find!(parent.id).name.should eq "Other parent"
end
it "does not update but raises an exception" do
parent = Parent.new(name: "New Parent")
parent.save!
expect_raises(Granite::RecordNotSaved, "Parent") do
parent.update!(name: "")
end
Parent.find!(parent.id).name.should eq "New Parent"
end
context "when skip_timestamps is true" do
it "does not update the updated_at field" do
time = Time.utc(2023, 9, 1)
parent = Parent.create(name: "New Parent")
parent.updated_at = time
parent.update!({name: "Other Parent"}, skip_timestamps: true)
Parent.find!(parent.id).updated_at.should eq time
end
end
end
================================================
FILE: spec/granite/validation_helpers/blank_spec.cr
================================================
require "../../spec_helper"
describe Granite::ValidationHelpers do
context "Blank" do
it "should work for is_blank and not_blank" do
blank_test = Validators::BlankTest.new
blank_test.first_name_not_blank = ""
blank_test.last_name_not_blank = " "
blank_test.first_name_is_blank = "foo"
blank_test.last_name_is_blank = " bar "
blank_test.save
blank_test.errors.size.should eq 4
blank_test.errors[0].message.should eq "first_name_not_blank must not be blank"
blank_test.errors[1].message.should eq "last_name_not_blank must not be blank"
blank_test.errors[2].message.should eq "first_name_is_blank must be blank"
blank_test.errors[3].message.should eq "last_name_is_blank must be blank"
end
end
end
================================================
FILE: spec/granite/validation_helpers/choice_spec.cr
================================================
require "../../spec_helper"
describe Granite::ValidationHelpers do
context "Choice" do
it "should work for is_valid_choice" do
choice_test = Validators::ChoiceTest.new
choice_test.number_symbol = 4
choice_test.type_array_symbol = "foo"
choice_test.number_string = 2
choice_test.type_array_string = "bar"
choice_test.save
choice_test.errors.size.should eq 4
choice_test.errors[0].message.should eq "number_symbol has an invalid choice. Valid choices are: 1,2,3"
choice_test.errors[1].message.should eq "type_array_symbol has an invalid choice. Valid choices are: internal,external,third_party"
choice_test.errors[2].message.should eq "number_string has an invalid choice. Valid choices are: 4,5,6"
choice_test.errors[3].message.should eq "type_array_string has an invalid choice. Valid choices are: internal,external,third_party"
end
end
end
================================================
FILE: spec/granite/validation_helpers/exclusion_spec.cr
================================================
require "../../spec_helper"
describe Granite::ValidationHelpers do
context "Exclusion" do
it "should allow non reserved words" do
exclusion = Validators::ExclusionTest.new
exclusion.name = "none_conflicting"
exclusion.save
exclusion.errors.size.should eq 0
end
it "should disallow reservered words" do
exclusion = Validators::ExclusionTest.new
exclusion.name = "test_name"
exclusion.save
exclusion.errors.size.should eq 1
exclusion.errors[0].message.should eq "Name got reserved values. Reserved values are test_name"
end
end
end
================================================
FILE: spec/granite/validation_helpers/inequality_spec.cr
================================================
require "../../spec_helper"
describe Granite::ValidationHelpers do
context "Less Than" do
it "should work for less_than" do
less_than_test = Validators::LessThanTest.new
less_than_test.int_32_lt = 10
less_than_test.float_32_lt = 20.5.to_f32
less_than_test.int_32_lte = 52
less_than_test.float_32_lte = 155.55.to_f32
less_than_test.save
less_than_test.errors.size.should eq 4
less_than_test.errors[0].message.should eq "int_32_lt must be less than 10"
less_than_test.errors[1].message.should eq "float_32_lt must be less than 20.5"
less_than_test.errors[2].message.should eq "int_32_lte must be less than or equal to 50"
less_than_test.errors[3].message.should eq "float_32_lte must be less than or equal to 100.25"
less_than_test_nil = Validators::LessThanTest.new
expect_raises(Exception, "Nil assertion failed") do
less_than_test_nil.save
end
end
end
context "Greater Than" do
it "should work for greater_than" do
greater_than_test = Validators::GreaterThanTest.new
greater_than_test.int_32_lt = 10
greater_than_test.float_32_lt = 20.5.to_f32
greater_than_test.int_32_lte = 49
greater_than_test.float_32_lte = 100.20.to_f32
greater_than_test.save
greater_than_test.errors.size.should eq 4
greater_than_test.errors[0].message.should eq "int_32_lt must be greater than 10"
greater_than_test.errors[1].message.should eq "float_32_lt must be greater than 20.5"
greater_than_test.errors[2].message.should eq "int_32_lte must be greater than or equal to 50"
greater_than_test.errors[3].message.should eq "float_32_lte must be greater than or equal to 100.25"
greater_than_test = Validators::GreaterThanTest.new
expect_raises(Exception, "Nil assertion failed") do
greater_than_test.save
end
end
end
end
================================================
FILE: spec/granite/validation_helpers/lenght_spec.cr
================================================
require "../../spec_helper"
describe Granite::ValidationHelpers do
context "Length" do
it "should work for length" do
length_test = Validators::LengthTest.new
length_test.title = "one"
length_test.description = "abcdefghijklmnopqrstuvwxyz"
length_test.save
length_test.errors.size.should eq 2
length_test.errors[0].message.should eq "title is too short. It must be at least 5"
length_test.errors[1].message.should eq "description is too long. It must be at most 25"
end
end
end
================================================
FILE: spec/granite/validation_helpers/nil_spec.cr
================================================
require "../../spec_helper"
describe Granite::ValidationHelpers do
context "Nil" do
it "should work for is_nil and not_nil for all data types" do
nil_test = Validators::NilTest.new
nil_test.first_name = "John"
nil_test.last_name = "Smith"
nil_test.age = 32
nil_test.born = true
nil_test.value = 123.56.to_f32
nil_test.save
nil_test.errors.size.should eq 10
nil_test.errors[0].message.should eq "first_name_not_nil must not be nil"
nil_test.errors[1].message.should eq "last_name_not_nil must not be nil"
nil_test.errors[2].message.should eq "age_not_nil must not be nil"
nil_test.errors[3].message.should eq "born_not_nil must not be nil"
nil_test.errors[4].message.should eq "value_not_nil must not be nil"
nil_test.errors[5].message.should eq "first_name must be nil"
nil_test.errors[6].message.should eq "last_name must be nil"
nil_test.errors[7].message.should eq "age must be nil"
nil_test.errors[8].message.should eq "born must be nil"
nil_test.errors[9].message.should eq "value must be nil"
end
end
end
================================================
FILE: spec/granite/validation_helpers/uniqueness_spec.cr
================================================
require "../../spec_helper"
describe Granite::ValidationHelpers do
context "Uniqueness" do
before_each do
Validators::PersonUniqueness.migrator.drop_and_create
end
it "should work for uniqueness" do
person_uniqueness1 = Validators::PersonUniqueness.new
person_uniqueness2 = Validators::PersonUniqueness.new
person_uniqueness1.name = "awesomeName"
person_uniqueness2.name = "awesomeName"
person_uniqueness1.save
person_uniqueness2.save
person_uniqueness1.errors.size.should eq 0
person_uniqueness2.errors.size.should eq 1
person_uniqueness2.errors[0].message.should eq "name should be unique"
end
it "should work for uniqueness on the same instance" do
person_uniqueness1 = Validators::PersonUniqueness.new
person_uniqueness1.name = "awesomeName"
person_uniqueness1.save
person_uniqueness1.errors.size.should eq 0
person_uniqueness1.name = "awesomeName"
person_uniqueness1.save
person_uniqueness1.errors.size.should eq 0
end
end
end
================================================
FILE: spec/granite/validations/validator_spec.cr
================================================
require "../../spec_helper"
{% begin %}
{% adapter_literal = env("CURRENT_ADAPTER").id %}
class NameTest < Granite::Base
connection {{ adapter_literal }}
column id : Int64, primary: true
column name : String?
validate :name, "cannot be blank", ->(s : NameTest) do
!s.name.to_s.blank?
end
end
class EmailTest < Granite::Base
connection {{ adapter_literal }}
column id : Int64, primary: true
column email : String?
validate :email, "cannot be blank" do |email_test|
!email_test.email.to_s.blank?
end
end
class PasswordTest < Granite::Base
connection {{ adapter_literal }}
column id : Int64, primary: true
column password : String?
column password_validation : String?
validate "password and validation should match" do |password_test|
password_test.password == password_test.password_validation
end
end
describe Granite::Validators do
describe "validates using proc" do
it "returns true if name is set" do
subject = NameTest.new
subject.name = "name"
subject.valid?.should eq true
end
it "returns false if name is blank" do
subject = NameTest.new
subject.name = ""
subject.valid?.should eq false
end
end
describe "validates using block" do
it "returns true if email is set" do
subject = EmailTest.new
subject.email = "test@example.com"
subject.valid?.should eq true
end
it "returns false if email is blank" do
subject = EmailTest.new
subject.email = ""
subject.valid?.should eq false
end
end
describe "validates using block without field" do
it "returns true if passwords match" do
subject = PasswordTest.new
subject.password = "123"
subject.password_validation = "123"
subject.valid?.should eq true
end
it "returns false if password does not match" do
subject = PasswordTest.new
subject.password = "123"
subject.password_validation = "1234"
subject.valid?.should eq false
end
end
describe "validates cleanly after previously failing" do
it "returns true if name is rectified after after failing" do
subject = NameTest.new
subject.name = ""
subject.valid?.should eq false
subject.name = "name"
subject.valid?.should eq true
end
end
end
{% end %}
================================================
FILE: spec/granite_spec.cr
================================================
require "./spec_helper"
class SomeClass
def initialize(@model_class : Granite::Base.class); end
def valid? : Bool
@model_class.exists? 123
end
def table : String
@model_class.table_name
end
end
describe Granite::Base do
it "class methods should work when type restricted to `Granite::Base`" do
f = SomeClass.new(Teacher)
f.valid?.should be_false
f.table.should eq "teachers"
end
it "should allow false as a column value" do
model = BoolModel.create active: false
model.active.should be_false
model.id.should eq 1
fetched_model = BoolModel.find! model.id
fetched_model.active.should be_false
end
describe "instantiation" do
describe "with default values" do
it "should instaniate correctly" do
model = DefaultValues.new
model.name.should eq "Jim"
model.age.should eq 0.0
model.is_alive.should be_true
end
end
describe "with a named tuple" do
it "should instaniate correctly" do
model = DefaultValues.new name: "Fred", is_alive: false
model.name.should eq "Fred"
model.age.should eq 0.0
model.is_alive.should be_false
end
end
describe "with a hash" do
it "should instaniate correctly" do
model = DefaultValues.new({"name" => "Bob", "age" => 3.14})
model.name.should eq "Bob"
model.age.should eq 3.14
model.is_alive.should be_true
end
end
describe "with a UUID" do
it "should instaniate correctly" do
uuid = UUID.random
model = UUIDNaturalModel.new uuid: uuid, field_uuid: uuid
model.uuid.should be_a UUID?
model.field_uuid.should be_a UUID?
model.uuid.should eq uuid
model.field_uuid.should eq uuid
end
end
describe "with string numeric values" do
it "should instaniate correctly" do
model = StringConversion.new({"user_id" => "1", "int32" => "17", "float32" => "3.14", "float" => "92342.2342342"})
model.user_id.should be_a Int64
model.user_id.should eq 1
model.int32.should be_a Int32
model.int32.should eq 17
model.float32.should be_a Float32
model.float32.should eq 3.14_f32
model.float.should be_a Float64
model.float.should eq 92342.2342342
end
end
end
describe Log do
it "should be logged as DEBUG" do
backend = Log::MemoryBackend.new
Log.builder.bind "granite", :debug, backend
Person.first
backend.entries.first.severity.debug?.should be_true
backend.entries.first.message.should match /.*SELECT.*people.*id.*FROM.*people.*LIMIT.*1.*: .*\[\]/
end
it "should not be logged" do
a = 0
Log.for("granite.test").info { a = 1 }
a.should eq 0
end
end
describe "JSON" do
describe ".from_json" do
it "can create an object from json" do
json_str = %({"name": "json::anyReview","upvotes": 2, "sentiment": 1.23, "interest": 4.56, "published": true})
review = Review.from_json(json_str)
review.name.should eq "json::anyReview"
review.upvotes.should eq 2
review.sentiment.should eq 1.23_f32
review.interest.should eq 4.56
review.published.should eq true
review.created_at.should be_nil
end
it "can create an array of objects from json" do
json_str = %([{"name": "json1","upvotes": 2, "sentiment": 1.23, "interest": 4.56, "published": true},{"name": "json2","upvotes": 0, "sentiment": 5.00, "interest": 6.99, "published": false}])
review = Array(Review).from_json(json_str)
review[0].name.should eq "json1"
review[0].upvotes.should eq 2
review[0].sentiment.should eq 1.23_f32
review[0].interest.should eq 4.56
review[0].published.should be_true
review[0].created_at.should be_nil
review[1].name.should eq "json2"
review[1].upvotes.should eq 0
review[1].sentiment.should eq 5.00_f32
review[1].interest.should eq 6.99
review[1].published.should be_false
review[1].created_at.should be_nil
end
it "works with after_initialize" do
model = AfterInit.from_json(%({"name": "after_initialize"}))
model.name.should eq "after_initialize"
model.priority.should eq 1000
end
describe "with default values" do
it "correctly applies values" do
model = DefaultValues.from_json(%({"name": "Bob"}))
model.name.should eq "Bob"
model.age.should eq 0.0
model.is_alive.should be_true
end
end
end
describe "#to_json" do
it "emits nil values when told" do
t = TodoEmitNull.new(name: "test todo", priority: 20)
result = %({"id":null,"name":"test todo","priority":20,"created_at":null,"updated_at":null})
t.to_json.should eq result
end
it "does not emit nil values by default" do
t = Todo.new(name: "test todo", priority: 20)
result = %({"name":"test todo","priority":20})
t.to_json.should eq result
end
it "works with array of models" do
todos = [
Todo.new(name: "todo 1", priority: 1),
Todo.new(name: "todo 2", priority: 2),
Todo.new(name: "todo 3", priority: 3),
]
collection = todos.to_json
collection.should eq %([{"name":"todo 1","priority":1},{"name":"todo 2","priority":2},{"name":"todo 3","priority":3}])
end
end
context "with json_options" do
model = TodoJsonOptions.from_json(%({"task_name": "The Task", "priority": 9000}))
it "should deserialize correctly" do
model.name.should eq "The Task"
model.priority.should be_nil
end
it "should serialize correctly" do
model.to_json.should eq %({"task_name":"The Task"})
end
describe "when using timestamp fields" do
TodoJsonOptions.import([
TodoJsonOptions.new(name: "first todo", priority: 200),
TodoJsonOptions.new(name: "second todo", priority: 500),
TodoJsonOptions.new(name: "third todo", priority: 300),
])
it "should serialize correctly" do
todos = TodoJsonOptions.order(id: :asc).select
todos[0].to_json.should eq %({"id":1,"task_name":"first todo","posted":"#{Time::Format::RFC_3339.format(todos[0].created_at!)}"})
todos[1].to_json.should eq %({"id":2,"task_name":"second todo","posted":"#{Time::Format::RFC_3339.format(todos[1].created_at!)}"})
todos[2].to_json.should eq %({"id":3,"task_name":"third todo","posted":"#{Time::Format::RFC_3339.format(todos[2].created_at!)}"})
end
end
end
end
describe "YAML" do
describe ".from_yaml" do
it "can create an object from YAML" do
yaml_str = %(---\nname: yaml::anyReview\nupvotes: 2\nsentiment: 1.23\ninterest: 4.56\npublished: true)
review = Review.from_yaml(yaml_str)
review.name.should eq "yaml::anyReview"
review.upvotes.should eq 2
review.sentiment.should eq 1.23.to_f32
review.interest.should eq 4.56
review.published.should eq true
review.created_at.should be_nil
end
it "can create an array of objects from YAML" do
yaml_str = "---\n- name: yaml1\n upvotes: 2\n sentiment: 1.23\n interest: 4.56\n published: true\n- name: yaml2\n upvotes: 0\n sentiment: !!float 5\n interest: 6.99\n published: false"
review = Array(Review).from_yaml(yaml_str)
review[0].name.should eq "yaml1"
review[0].upvotes.should eq 2
review[0].sentiment.should eq 1.23.to_f32
review[0].interest.should eq 4.56
review[0].published.should be_true
review[0].created_at.should be_nil
review[1].name.should eq "yaml2"
review[1].upvotes.should eq 0
review[1].sentiment.should eq 5.00.to_f32
review[1].interest.should eq 6.99
review[1].published.should be_false
review[1].created_at.should be_nil
end
it "works with after_initialize" do
model = AfterInit.from_yaml(%(---\nname: after_initialize))
model.name.should eq "after_initialize"
model.priority.should eq 1000
end
describe "with default values" do
it "correctly applies values" do
model = DefaultValues.from_yaml(%(---\nname: Bob))
model.name.should eq "Bob"
model.age.should eq 0.0
model.is_alive.should be_true
end
end
end
describe "#to_yaml" do
it "emits nil values when told" do
t = TodoEmitNull.new(name: "test todo", priority: 20)
result = %(---\nid:\nname: test todo\npriority: 20\ncreated_at:\nupdated_at:\n)
t.to_yaml.should eq result
end
it "does not emit nil values by default" do
t = Todo.new(name: "test todo", priority: 20)
result = %(---\nname: test todo\npriority: 20\n)
t.to_yaml.should eq result
end
it "works with array of models" do
todos = [
Todo.new(name: "todo 1", priority: 1),
Todo.new(name: "todo 2", priority: 2),
Todo.new(name: "todo 3", priority: 3),
]
collection = todos.to_yaml
collection.should eq %(---\n- name: todo 1\n priority: 1\n- name: todo 2\n priority: 2\n- name: todo 3\n priority: 3\n)
end
end
context "with yaml_options" do
model = TodoYamlOptions.from_yaml(%(---\ntask_name: The Task\npriority: 9000))
it "should deserialize correctly" do
model.name.should eq "The Task"
model.priority.should be_nil
end
it "should serialize correctly" do
model.to_yaml.should eq %(---\ntask_name: The Task\n)
end
describe "when using timestamp fields" do
TodoYamlOptions.import([
TodoYamlOptions.new(name: "first todo", priority: 200),
TodoYamlOptions.new(name: "second todo", priority: 500),
TodoYamlOptions.new(name: "third todo", priority: 300),
])
it "should serialize correctly" do
todos = TodoYamlOptions.order(id: :asc).select
todos[0].to_yaml.should eq %(---\nid: 1\ntask_name: first todo\nposted: #{Time::Format::YAML_DATE.format(todos[0].created_at!)}\n)
todos[1].to_yaml.should eq %(---\nid: 2\ntask_name: second todo\nposted: #{Time::Format::YAML_DATE.format(todos[1].created_at!)}\n)
todos[2].to_yaml.should eq %(---\nid: 3\ntask_name: third todo\nposted: #{Time::Format::YAML_DATE.format(todos[2].created_at!)}\n)
end
end
end
end
describe "#to_h" do
it "convert object to hash" do
t = Todo.new(name: "test todo", priority: 20)
result = {"id" => nil, "name" => "test todo", "priority" => 20, "created_at" => nil, "updated_at" => nil}
t.to_h.should eq result
end
it "honors custom primary key" do
s = Item.new(item_name: "Hacker News")
s.item_id = "three"
s.to_h.should eq({"item_name" => "Hacker News", "item_id" => "three"})
end
it "works with enums" do
model = EnumModel.new
model.my_enum = MyEnum::One
model.to_h.should eq({"id" => nil, "my_enum" => MyEnum::One})
end
end
# Only PG supports array types
{% if env("CURRENT_ADAPTER") == "pg" %}
describe "Array(T)" do
describe "with values" do
it "should instantiate correctly" do
model = ArrayModel.new str_array: ["foo", "bar"]
model.str_array.should eq ["foo", "bar"]
end
it "should save correctly" do
model = ArrayModel.new
model.id = 1
model.str_array = ["jack", "john", "jill"]
model.i16_array = [10_000_i16, 20_000_i16, 30_000_i16]
model.i32_array = [1_000_000_i32, 2_000_000_i32, 3_000_000_i32, 4_000_000_i32]
model.i64_array = [100_000_000_000_i64, 200_000_000_000_i64, 300_000_000_000_i64, 400_000_000_000_i64]
model.f32_array = [1.123_456_78_f32, 1.234_567_899_998_741_4_f32]
model.f64_array = [1.123_456_789_011_23_f64, 1.234_567_899_998_741_4_f64]
model.bool_array = [true, true, false, true, false, false]
model.save.should be_true
end
it "should read correctly" do
model = ArrayModel.find! 1
model.str_array!.should be_a Array(String)
model.str_array!.should eq ["jack", "john", "jill"]
model.i16_array!.should be_a Array(Int16)
model.i16_array!.should eq [10_000_i16, 20_000_i16, 30_000_i16]
model.i32_array!.should be_a Array(Int32)
model.i32_array!.should eq [1_000_000_i32, 2_000_000_i32, 3_000_000_i32, 4_000_000_i32]
model.i64_array!.should be_a Array(Int64)
model.i64_array!.should eq [100_000_000_000_i64, 200_000_000_000_i64, 300_000_000_000_i64, 400_000_000_000_i64]
model.f32_array!.should be_a Array(Float32)
model.f32_array!.should eq [1.123_456_78_f32, 1.234_567_899_998_741_4_f32]
model.f64_array!.should be_a Array(Float64)
model.f64_array!.should eq [1.123_456_789_011_23_f64, 1.234_567_899_998_741_4_f64]
model.bool_array!.should be_a Array(Bool)
model.bool_array!.should eq [true, true, false, true, false, false]
end
end
describe "with empty array" do
it "should save correctly" do
model = ArrayModel.new
model.id = 2
model.str_array = [] of String
model.f64_array.should be_a(Array(Float64))
model.f64_array.should eq [] of Float64
model.save.should be_true
end
it "should read correctly" do
model = ArrayModel.find! 2
model.str_array.should be_a Array(String)?
model.str_array!.should eq [] of String
model.i16_array.should be_nil
model.i32_array.should be_nil
model.i64_array.should be_nil
model.f32_array.should be_nil
model.f64_array.should be_a(Array(Float64))
model.f64_array.should eq [] of Float64
model.bool_array.should be_nil
end
end
end
{% end %}
end
================================================
FILE: spec/mocks/db_mock.cr
================================================
class FakeStatement < DB::Statement
protected def perform_query(args : Enumerable) : DB::ResultSet
FieldEmitter.new
end
protected def perform_exec(args : Enumerable) : DB::ExecResult
DB::ExecResult.new 0_i64, 0_i64
end
end
class FakeContext
include DB::ConnectionContext
def uri : URI
URI.new ""
end
def prepared_statements? : Bool
false
end
def discard(connection); end
def release(connection); end
end
class FakeConnection < DB::Connection
def initialize
super(DB::Connection::Options.new)
@context = FakeContext.new
@prepared_statements = false
end
def build_unprepared_statement(query) : FakeStatement
FakeStatement.new self, query
end
def build_prepared_statement(query) : FakeStatement
FakeStatement.new self, query
end
end
alias EmitterType = DB::Any | PG::Numeric | JSON::Any | Int16
# FieldEmitter emulates the subtle and uninformed way that
# DB::ResultSet emits data. To be used in testing interactions
# with raw data sets.
class FieldEmitter < DB::ResultSet
# 1. Override `#move_next` to move to the next row.
# 2. Override `#read` returning the next value in the row.
# 3. (Optional) Override `#read(t)` for some types `t` for which custom logic other than a simple cast is needed.
# 4. Override `#column_count`, `#column_name`.
@position = 0
@field_position = 0
@values = [] of EmitterType
def initialize
@statement = FakeStatement.new FakeConnection.new, ""
end
def _set_values(values : Array(EmitterType))
@values = [] of EmitterType
values.each do |v|
@values << v
end
end
def move_next : Bool
@position += 1
@field_position = 0
@position < @values.size
end
def read
if @position >= @values.size
raise "Overread"
end
@values[@position].tap do
@position += 1
end
end
def column_count : Int32
@values.size
end
def column_name(index : Int32) : String
"Column #{index}"
end
def next_column_index : Int32
@field_position
end
end
================================================
FILE: spec/run_all_specs.sh
================================================
#! /bin/bash
source .env
echo "Testing PG"
docker-compose -f docker/docker-compose.pg.yml build spec
docker-compose -f docker/docker-compose.pg.yml run spec
echo "Testing mysql"
docker-compose -f docker/docker-compose.mysql.yml build spec
docker-compose -f docker/docker-compose.mysql.yml run spec
echo "Testing sqlite"
docker-compose -f docker/docker-compose.sqlite.yml build spec
docker-compose -f docker/docker-compose.sqlite.yml run spec
echo "Done testing...stopping/removing images"
docker-compose -f docker/docker-compose.sqlite.yml down
docker-compose -f docker/docker-compose.mysql.yml down
docker-compose -f docker/docker-compose.pg.yml down
================================================
FILE: spec/run_test_dbs.sh
================================================
#!/bin/bash
MYSQL_VERSION=${MYSQL_VERSION:-5.7}
PG_VERSION=${PG_VERSION:-15.2}
docker run --name mysql -d \
-e MYSQL_ROOT_PASSWORD=password \
-e MYSQL_DATABASE=granite_db \
-e MYSQL_USER=granite \
-e MYSQL_PASSWORD=password \
-p 3306:3306 \
mysql:${MYSQL_VERSION}
docker run --name psql -d \
-e POSTGRES_USER=granite \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=granite_db \
-p 5432:5432 \
postgres:${PG_VERSION}
================================================
FILE: spec/spec_helper.cr
================================================
require "mysql"
require "pg"
require "sqlite3"
CURRENT_ADAPTER = ENV["CURRENT_ADAPTER"]
ADAPTER_URL = ENV["#{CURRENT_ADAPTER.upcase}_DATABASE_URL"]
ADAPTER_REPLICA_URL = ENV["#{CURRENT_ADAPTER.upcase}_REPLICA_URL"]? || ADAPTER_URL
case CURRENT_ADAPTER
when "pg"
Granite::Connections << Granite::Adapter::Pg.new(name: CURRENT_ADAPTER, url: ADAPTER_URL)
Granite::Connections << {name: "pg_with_replica", writer: ADAPTER_URL, reader: ADAPTER_REPLICA_URL, adapter_type: Granite::Adapter::Pg}
when "mysql"
Granite::Connections << Granite::Adapter::Mysql.new(name: CURRENT_ADAPTER, url: ADAPTER_URL)
Granite::Connections << {name: "mysql_with_replica", writer: ADAPTER_URL, reader: ADAPTER_REPLICA_URL, adapter_type: Granite::Adapter::Mysql}
when "sqlite"
Granite::Connections << Granite::Adapter::Sqlite.new(name: CURRENT_ADAPTER, url: ADAPTER_URL)
Granite::Connections << {name: "sqlite_with_replica", writer: ADAPTER_URL, reader: ADAPTER_REPLICA_URL, adapter_type: Granite::Adapter::Sqlite}
when Nil
raise "Please set CURRENT_ADAPTER"
else
raise "Unknown adapter #{CURRENT_ADAPTER}"
end
require "spec"
require "../src/granite"
require "../src/adapter/**"
require "./spec_models"
require "./mocks/**"
Spec.before_suite do
Granite.settings.default_timezone = Granite::TIME_ZONE
{% if flag?(:spec_logs) %}
::Log.builder.bind(
# source: "spec.client",
source: "*",
level: ::Log::Severity::Trace,
backend: ::Log::IOBackend.new(STDOUT, dispatcher: :sync),
)
{% end %}
end
Spec.before_each do
# I have no idea why this is needed, but it is.
Granite.settings.default_timezone = Granite::TIME_ZONE
end
{% if env("CURRENT_ADAPTER") == "mysql" && !flag?(:issue_473) %}
Spec.after_each do
# https://github.com/amberframework/granite/issues/473
Granite::Connections["mysql"].not_nil![:writer].try &.database.pool.close
end
{% end %}
================================================
FILE: spec/spec_models.cr
================================================
require "uuid"
class Granite::Base
def self.drop_and_create
end
end
{% begin %}
{% adapter_literal = env("CURRENT_ADAPTER").id %}
class ReplicatedChat < Granite::Base
connection {{ "#{adapter_literal}_with_replica" }}
table replicated_chats
column id : Int64, primary: true
column content : String
end
class Chat < Granite::Base
connection {{ adapter_literal }}
table chats
column id : Int64, primary: true
column name : String
has_one settings : ChatSettings, foreign_key: :chat_id
end
class ChatSettings < Granite::Base
connection {{ adapter_literal }}
table chat_settings
belongs_to chat : Chat, primary: true
column flood_limit : Int32
end
class Parent < Granite::Base
connection {{ adapter_literal }}
table parents
column id : Int64, primary: true
column name : String?
timestamps
has_many :students, class_name: Student
validate :name, "Name cannot be blank" do |parent|
!parent.name.to_s.blank?
end
end
class Teacher < Granite::Base
connection {{ adapter_literal }}
table teachers
column id : Int64, primary: true
column name : String?
has_many :klasses, class_name: Klass
end
class Student < Granite::Base
connection {{ adapter_literal }}
table students
column id : Int64, primary: true
column name : String?
has_many :enrollments, class_name: Enrollment
has_many :klasses, class_name: Klass, through: :enrollments
end
class Klass < Granite::Base
connection {{ adapter_literal }}
table klasses
column id : Int64, primary: true
column name : String?
belongs_to teacher : Teacher
has_many :enrollments, class_name: Enrollment
has_many :students, class_name: Student, through: :enrollments
end
class Enrollment < Granite::Base
connection {{ adapter_literal }}
table enrollments
column id : Int64, primary: true
belongs_to :student
belongs_to :klass
end
class School < Granite::Base
connection {{ adapter_literal }}
table schools
column custom_id : Int64, primary: true
column name : String?
end
class User < Granite::Base
connection {{ adapter_literal }}
table users
column id : Int64, primary: true
column email : String?
has_one :profile
end
class Character < Granite::Base
connection {{ adapter_literal }}
table characters
column character_id : Int32, primary: true
column name : String
end
class Courier < Granite::Base
connection {{ adapter_literal }}
table couriers
column courier_id : Int32, primary: true, auto: false
column issuer_id : Int32
belongs_to service : CourierService, primary_key: "owner_id"
has_one issuer : Character, primary_key: "issuer_id", foreign_key: "character_id"
end
class CourierService < Granite::Base
connection {{ adapter_literal }}
table services
column owner_id : Int64, primary: true, auto: false
column name : String
has_many :couriers, class_name: Courier, foreign_key: "service_id"
end
class Profile < Granite::Base
connection {{ adapter_literal }}
table profiles
column id : Int64, primary: true
column name : String?
belongs_to :user
end
class Nation::County < Granite::Base
connection {{ adapter_literal }}
table nation_counties
column id : Int64, primary: true
column name : String?
end
class Review < Granite::Base
connection {{ adapter_literal }}
table reviews
column id : Int64, primary: true
column name : String?
column downvotes : Int32?
column upvotes : Int64?
column sentiment : Float32?
column interest : Float64?
column published : Bool?
column created_at : Time?
end
class Empty < Granite::Base
connection {{ adapter_literal }}
table empties
column id : Int64, primary: true
end
class ReservedWord < Granite::Base
connection {{ adapter_literal }}
table "select"
column id : Int64, primary: true
column all : String?
end
class Callback < Granite::Base
connection {{ adapter_literal }}
table callbacks
column id : Int64, primary: true
column name : String?
property history : IO::Memory = IO::Memory.new
{% for name in Granite::Callbacks::CALLBACK_NAMES %}
{{name.id}} _{{name.id}}
private def _{{name.id}}
history << "{{name.id}}\n"
end
{% end %}
end
class CallbackWithAbort < Granite::Base
connection {{ adapter_literal }}
table callbacks_with_abort
column abort_at : String, primary: true, auto: false
column do_abort : Bool?
column name : String?
property history : IO::Memory = IO::Memory.new
{% for name in Granite::Callbacks::CALLBACK_NAMES %}
{{name.id}} do
abort! if do_abort && abort_at == "{{name.id}}"
history << "{{name.id}}\n"
end
{% end %}
end
class Kvs < Granite::Base
connection {{ adapter_literal }}
table kvs
column k : String, primary: true, auto: false
column v : String?
end
class Person < Granite::Base
connection {{ adapter_literal }}
table people
column id : Int64, primary: true
column name : String?
end
class Company < Granite::Base
connection {{ adapter_literal }}
table companies
column id : Int32, primary: true
column name : String?
end
class Book < Granite::Base
connection {{ adapter_literal }}
table books
column id : Int32, primary: true
column name : String?
@[JSON::Field(ignore: true)]
@[YAML::Field(ignore: true)]
belongs_to publisher : Company, foreign_key: publisher_id : Int32?
has_many :book_reviews, class_name: BookReview
belongs_to author : Person
end
class BookReview < Granite::Base
connection {{ adapter_literal }}
table book_reviews
column id : Int32, primary: true
column body : String?
belongs_to book : Book, foreign_key: book_id : Int32?
end
class Item < Granite::Base
connection {{ adapter_literal }}
table items
column item_id : String, primary: true, auto: false
column item_name : String?
before_create :generate_uuid
gitextract_almbvcqk/
├── .dockerignore
├── .envrc
├── .github/
│ └── workflows/
│ └── spec.yml
├── .gitignore
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── docker/
│ ├── docker-compose.mysql.yml
│ ├── docker-compose.pg.yml
│ └── docker-compose.sqlite.yml
├── docker-compose.yml
├── docs/
│ ├── callbacks.md
│ ├── crud.md
│ ├── imports.md
│ ├── migrations.md
│ ├── models.md
│ ├── multiple_connections.md
│ ├── querying.md
│ ├── readme.md
│ ├── relationships.md
│ └── validations.md
├── export.sh
├── shard.yml
├── spec/
│ ├── adapter/
│ │ ├── adapters_spec.cr
│ │ ├── mysql_spec.cr
│ │ ├── pg_spec.cr
│ │ └── sqlite_spec.cr
│ ├── granite/
│ │ ├── associations/
│ │ │ ├── belongs_to_spec.cr
│ │ │ ├── has_many_spec.cr
│ │ │ ├── has_many_through_spec.cr
│ │ │ └── has_one_spec.cr
│ │ ├── callbacks/
│ │ │ ├── abort_spec.cr
│ │ │ └── callbacks_spec.cr
│ │ ├── columns/
│ │ │ ├── primary_key_spec.cr
│ │ │ ├── read_attribute_spec.cr
│ │ │ ├── timestamps_spec.cr
│ │ │ └── uuid_spec.cr
│ │ ├── connection_management_spec.cr
│ │ ├── converters/
│ │ │ ├── converters_spec.cr
│ │ │ ├── enum_spec.cr
│ │ │ ├── json_spec.cr
│ │ │ └── pg_numeric_spec.cr
│ │ ├── error/
│ │ │ └── error_spec.cr
│ │ ├── exceptions/
│ │ │ ├── record_invalid_spec.cr
│ │ │ └── record_not_destroyed_spec.cr
│ │ ├── integrators/
│ │ │ └── find_or_spec.cr
│ │ ├── migrator/
│ │ │ └── migrator_spec.cr
│ │ ├── query/
│ │ │ ├── assemblers/
│ │ │ │ ├── mysql_spec.cr
│ │ │ │ ├── pg_spec.cr
│ │ │ │ └── sqlite_spec.cr
│ │ │ ├── builder_spec.cr
│ │ │ ├── executor_spec.cr
│ │ │ └── spec_helper.cr
│ │ ├── querying/
│ │ │ ├── all_spec.cr
│ │ │ ├── count_spec.cr
│ │ │ ├── exists_spec.cr
│ │ │ ├── find_by_spec.cr
│ │ │ ├── find_each_spec.cr
│ │ │ ├── find_in_batches.cr
│ │ │ ├── find_spec.cr
│ │ │ ├── first_spec.cr
│ │ │ ├── from_rs_spec.cr
│ │ │ ├── passthrough_spec.cr
│ │ │ ├── query_builder_spec.cr
│ │ │ └── reload_spec.cr
│ │ ├── select/
│ │ │ └── select_spec.cr
│ │ ├── table/
│ │ │ └── table_spec.cr
│ │ ├── transactions/
│ │ │ ├── create_spec.cr
│ │ │ ├── destroy_spec.cr
│ │ │ ├── import_spec.cr
│ │ │ ├── save_natural_key_spec.cr
│ │ │ ├── save_spec.cr
│ │ │ ├── touch_spec.cr
│ │ │ └── update_spec.cr
│ │ ├── validation_helpers/
│ │ │ ├── blank_spec.cr
│ │ │ ├── choice_spec.cr
│ │ │ ├── exclusion_spec.cr
│ │ │ ├── inequality_spec.cr
│ │ │ ├── lenght_spec.cr
│ │ │ ├── nil_spec.cr
│ │ │ └── uniqueness_spec.cr
│ │ └── validations/
│ │ └── validator_spec.cr
│ ├── granite_spec.cr
│ ├── mocks/
│ │ └── db_mock.cr
│ ├── run_all_specs.sh
│ ├── run_test_dbs.sh
│ ├── spec_helper.cr
│ └── spec_models.cr
└── src/
├── adapter/
│ ├── base.cr
│ ├── mysql.cr
│ ├── pg.cr
│ └── sqlite.cr
├── granite/
│ ├── association_collection.cr
│ ├── associations.cr
│ ├── base.cr
│ ├── callbacks.cr
│ ├── collection.cr
│ ├── columns.cr
│ ├── connection_management.cr
│ ├── connections.cr
│ ├── converters.cr
│ ├── error.cr
│ ├── exceptions.cr
│ ├── integrators.cr
│ ├── migrator.cr
│ ├── query/
│ │ ├── assemblers/
│ │ │ ├── base.cr
│ │ │ ├── mysql.cr
│ │ │ ├── pg.cr
│ │ │ └── sqlite.cr
│ │ ├── builder.cr
│ │ ├── builder_methods.cr
│ │ └── executors/
│ │ ├── base.cr
│ │ ├── list.cr
│ │ ├── multi_value.cr
│ │ └── value.cr
│ ├── querying.cr
│ ├── select.cr
│ ├── settings.cr
│ ├── table.cr
│ ├── transactions.cr
│ ├── type.cr
│ ├── validation_helpers/
│ │ ├── blank.cr
│ │ ├── choice.cr
│ │ ├── exclusion.cr
│ │ ├── inequality.cr
│ │ ├── length.cr
│ │ ├── nil.cr
│ │ └── uniqueness.cr
│ ├── validators.cr
│ └── version.cr
└── granite.cr
Condensed preview — 132 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (311K chars).
[
{
"path": ".dockerignore",
"chars": 4,
"preview": "bin\n"
},
{
"path": ".envrc",
"chars": 17,
"preview": "dotenv_if_exists\n"
},
{
"path": ".github/workflows/spec.yml",
"chars": 3675,
"preview": "name: spec\non:\n push:\n pull_request:\n branches: [main, master]\n # schedule:\n # - cron: \"0 6 * * 6\" # Every Satu"
},
{
"path": ".gitignore",
"chars": 239,
"preview": "/lib/\n/.shards/\n/.deps/\n/.crystal/\n/doc/\n*.db\n\n# Libraries don't need dependency lock\n# Dependencies will be locked in a"
},
{
"path": ".travis.yml",
"chars": 1226,
"preview": "language: generic\n\nservices:\n - docker\n\nbefore_install:\n - docker-compose -f docker/docker-compose.$CURRENT_ADAPTER.ym"
},
{
"path": "Dockerfile",
"chars": 324,
"preview": "FROM 84codes/crystal:latest-ubuntu-jammy\n\n# Install deps\nRUN apt-get update -qq && apt-get install -y --no-install-recom"
},
{
"path": "LICENSE",
"chars": 1077,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2019 dru.jensen\n\nPermission is hereby granted, free of charge, to any person obtain"
},
{
"path": "README.md",
"chars": 3336,
"preview": "# Granite\n\n[Amber](https://github.com/amberframework/amber) is a web framework written in\nthe [Crystal](https://github.c"
},
{
"path": "docker/docker-compose.mysql.yml",
"chars": 343,
"preview": "version: '2'\nservices:\n spec:\n extends:\n file: ../docker-compose.yml\n service: spec\n environment:\n "
},
{
"path": "docker/docker-compose.pg.yml",
"chars": 255,
"preview": "version: '2'\nservices:\n spec:\n extends:\n file: ../docker-compose.yml\n service: spec\n environment:\n "
},
{
"path": "docker/docker-compose.sqlite.yml",
"chars": 175,
"preview": "version: '2'\nservices:\n spec:\n extends:\n file: ../docker-compose.yml\n service: spec\n build:\n conte"
},
{
"path": "docker-compose.yml",
"chars": 407,
"preview": "version: '2'\nservices:\n spec:\n build: .\n command: 'bash -c \"cd /app/user && bin/ameba && crystal tool format --ch"
},
{
"path": "docs/callbacks.md",
"chars": 671,
"preview": "# Callbacks\n\nCall a specified method on a specific life cycle event.\n\nHere is an example:\n\n```crystal\nrequire \"granite/a"
},
{
"path": "docs/crud.md",
"chars": 3357,
"preview": "# CRUD\n\n## Create\n\nCombination of object creation and insertion into database.\n\n```crystal\nPost.create(name: \"Granite Ro"
},
{
"path": "docs/imports.md",
"chars": 3482,
"preview": "# Bulk Insertions\n\n## Import\n\n> **Note:** Imports do not trigger callbacks automatically. See [Running Callbacks](#runni"
},
{
"path": "docs/migrations.md",
"chars": 1876,
"preview": "# Migrations\n\n## Database Migrations with micrate\n\nIf you're using Granite to query your data, you likely want to manage"
},
{
"path": "docs/models.md",
"chars": 5766,
"preview": "# Model Usage\n\n## Multiple Connections\n\nIt is possible to register multiple connections, for example:\n\n```crystal\nGranit"
},
{
"path": "docs/multiple_connections.md",
"chars": 1580,
"preview": "# Read replica support\n\nIn Granite, you can create a connection that has a write/read node. If this is done. Granite wil"
},
{
"path": "docs/querying.md",
"chars": 4471,
"preview": "# Querying\n\nThe query macro and where clause combine to give you full control over your query.\n\n## Where\n\nWhere is using"
},
{
"path": "docs/readme.md",
"chars": 1590,
"preview": "# Documentation\n\n## Getting Started\n\n### Installation\n\nAdd this library to your projects dependencies along with the dri"
},
{
"path": "docs/relationships.md",
"chars": 6003,
"preview": "# Relationships\n\n## One to One\n\nFor one-to-one relationships, You can use the `has_one` and `belongs_to` in your models."
},
{
"path": "docs/validations.md",
"chars": 2214,
"preview": "# Errors\n\nAll database errors are added to the `errors` array used by `Granite::Validators` with the symbol `:base`\n\n```"
},
{
"path": "export.sh",
"chars": 224,
"preview": "#!/bin/bash\n\nif [ -f .env ]; then\n while IFS= read -r line; do\n export \"$line\"\n done < .env\n echo \"Environme"
},
{
"path": "shard.yml",
"chars": 529,
"preview": "name: granite\n\nversion: 0.23.4\n\ncrystal: \">= 1.6.0, < 2.0.0\"\n\nauthors:\n - drujensen <drujensen@gmail.com>\n - elorest <"
},
{
"path": "spec/adapter/adapters_spec.cr",
"chars": 1705,
"preview": "require \"../spec_helper\"\n\nclass Foo < Granite::Base\n connection {{env(\"CURRENT_ADAPTER\").id}}\n column id : Int64, prim"
},
{
"path": "spec/adapter/mysql_spec.cr",
"chars": 25,
"preview": "require \"../spec_helper\"\n"
},
{
"path": "spec/adapter/pg_spec.cr",
"chars": 25,
"preview": "require \"../spec_helper\"\n"
},
{
"path": "spec/adapter/sqlite_spec.cr",
"chars": 25,
"preview": "require \"../spec_helper\"\n"
},
{
"path": "spec/granite/associations/belongs_to_spec.cr",
"chars": 3278,
"preview": "require \"../../spec_helper\"\n\ndescribe \"belongs_to\" do\n it \"provides a getter for the foreign entity\" do\n teacher = T"
},
{
"path": "spec/granite/associations/has_many_spec.cr",
"chars": 5148,
"preview": "require \"../../spec_helper\"\n\ndescribe \"has_many\" do\n it \"provides a method to retrieve associated objects\" do\n teach"
},
{
"path": "spec/granite/associations/has_many_through_spec.cr",
"chars": 5632,
"preview": "require \"../../spec_helper\"\n\ndescribe \"has_many, through:\" do\n it \"provides a method to retrieve associated objects thr"
},
{
"path": "spec/granite/associations/has_one_spec.cr",
"chars": 1502,
"preview": "require \"../../spec_helper\"\n\ndescribe \"has_one\" do\n before_each do\n User.clear\n Profile.clear\n Courier.clear\n "
},
{
"path": "spec/granite/callbacks/abort_spec.cr",
"chars": 4619,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#abort!\" do\n before_each do\n CallbackWithAbort.clear\n end\n\n context \"when c"
},
{
"path": "spec/granite/callbacks/callbacks_spec.cr",
"chars": 2141,
"preview": "require \"../../spec_helper\"\n\ndescribe \"(callback feature)\" do\n describe \"#save (new record)\" do\n it \"runs before_sav"
},
{
"path": "spec/granite/columns/primary_key_spec.cr",
"chars": 513,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#new\" do\n it \"works when the primary is defined as `auto: true`\" do\n Parent.n"
},
{
"path": "spec/granite/columns/read_attribute_spec.cr",
"chars": 252,
"preview": "require \"../../spec_helper\"\n\ndescribe \"read_attribute\" do\n # Only PG supports array types\n {% if env(\"CURRENT_ADAPTER\""
},
{
"path": "spec/granite/columns/timestamps_spec.cr",
"chars": 4489,
"preview": "require \"../../spec_helper\"\n\n# Can run this spec for sqlite after https://www.sqlite.org/draft/releaselog/3_24_0.html is"
},
{
"path": "spec/granite/columns/uuid_spec.cr",
"chars": 306,
"preview": "require \"../../spec_helper\"\n\ndescribe \"UUID creation\" do\n it \"correctly sets a RFC4122 V4 UUID on save\" do\n item = U"
},
{
"path": "spec/granite/connection_management_spec.cr",
"chars": 611,
"preview": "require \"spec\"\n\ndescribe \"Granite::Base track time since last write\" do\n it \"should switch to reader db connection afte"
},
{
"path": "spec/granite/converters/converters_spec.cr",
"chars": 8430,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::Converters do\n {% if env(\"CURRENT_ADAPTER\") == \"pg\" %}\n describe \"#sa"
},
{
"path": "spec/granite/converters/enum_spec.cr",
"chars": 2295,
"preview": "require \"../../spec_helper\"\n\nenum TestEnum\n Zero\n One\n Two\n Three = 17\nend\n\ndescribe Granite::Converters::Enum do\n "
},
{
"path": "spec/granite/converters/json_spec.cr",
"chars": 2030,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::Converters::Json do\n describe String do\n describe \".to_db\" do\n i"
},
{
"path": "spec/granite/converters/pg_numeric_spec.cr",
"chars": 526,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::Converters::PgNumeric do\n describe \".to_db\" do\n it \"should convert a "
},
{
"path": "spec/granite/error/error_spec.cr",
"chars": 213,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::Error do\n it \"should convert to json\" do\n Granite::Error.new(\"field\","
},
{
"path": "spec/granite/exceptions/record_invalid_spec.cr",
"chars": 466,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::RecordNotSaved do\n it \"should have a message\" do\n parent = Parent.new"
},
{
"path": "spec/granite/exceptions/record_not_destroyed_spec.cr",
"chars": 478,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::RecordNotDestroyed do\n it \"should have a message\" do\n parent = Parent"
},
{
"path": "spec/granite/integrators/find_or_spec.cr",
"chars": 871,
"preview": "require \"../../spec_helper\"\n\ndescribe \"find_or_create_by, find_or_initialize_by\" do\n it \"creates on find_or_create when"
},
{
"path": "spec/granite/migrator/migrator_spec.cr",
"chars": 5012,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::Migrator do\n describe \"#drop_sql\" do\n it \"generates correct SQL with "
},
{
"path": "spec/granite/query/assemblers/mysql_spec.cr",
"chars": 4714,
"preview": "require \"../spec_helper\"\n\n{% if env(\"CURRENT_ADAPTER\").id == \"mysql\" %}\n describe Granite::Query::Assembler::Mysql(Mode"
},
{
"path": "spec/granite/query/assemblers/pg_spec.cr",
"chars": 4716,
"preview": "require \"../spec_helper\"\n\n{% if env(\"CURRENT_ADAPTER\").id == \"pg\" %}\n describe Granite::Query::Assembler::Pg(Model) do\n"
},
{
"path": "spec/granite/query/assemblers/sqlite_spec.cr",
"chars": 4716,
"preview": "require \"../spec_helper\"\n\n{% if env(\"CURRENT_ADAPTER\").id == \"sqlite\" %}\n describe Granite::Query::Assembler::Sqlite(Mo"
},
{
"path": "spec/granite/query/builder_spec.cr",
"chars": 3066,
"preview": "require \"./spec_helper\"\n\ndescribe Granite::Query::Builder(Model) do\n it \"stores where_fields\" do\n query = builder.wh"
},
{
"path": "spec/granite/query/executor_spec.cr",
"chars": 63,
"preview": "# default value when a Value query is run\n# delegates properly\n"
},
{
"path": "spec/granite/query/spec_helper.cr",
"chars": 872,
"preview": "require \"spec\"\nrequire \"db\"\nrequire \"../../../src/granite/query/builder\"\n\nclass Model\n def self.table_name\n \"table\"\n"
},
{
"path": "spec/granite/querying/all_spec.cr",
"chars": 843,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#all\" do\n it \"finds all the records\" do\n Parent.clear\n model_ids = (0...10"
},
{
"path": "spec/granite/querying/count_spec.cr",
"chars": 365,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#count\" do\n it \"returns 0 if no result\" do\n Parent.clear\n count = Parent.c"
},
{
"path": "spec/granite/querying/exists_spec.cr",
"chars": 2342,
"preview": "require \"../../spec_helper\"\n\ndescribe \".exists?\" do\n before_each do\n Parent.clear\n end\n\n describe \"when there is a"
},
{
"path": "spec/granite/querying/find_by_spec.cr",
"chars": 2332,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#find_by, #find_by!\" do\n it \"finds an object with a string field\" do\n Parent."
},
{
"path": "spec/granite/querying/find_each_spec.cr",
"chars": 1382,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#find_each\" do\n it \"finds all the records\" do\n Parent.clear\n model_ids = ("
},
{
"path": "spec/granite/querying/find_in_batches.cr",
"chars": 1888,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#find_in_batches\" do\n it \"finds records in batches and yields all the records\" d"
},
{
"path": "spec/granite/querying/find_spec.cr",
"chars": 1585,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#find, #find!\" do\n it \"finds an object by id\" do\n model = Parent.new\n mode"
},
{
"path": "spec/granite/querying/first_spec.cr",
"chars": 1317,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#first, #first!\" do\n it \"finds the first object\" do\n Parent.clear\n first ="
},
{
"path": "spec/granite/querying/from_rs_spec.cr",
"chars": 637,
"preview": "require \"../../spec_helper\"\n\nmacro build_review_emitter\n FieldEmitter.new.tap do |e|\n e._set_values(\n [\n "
},
{
"path": "spec/granite/querying/passthrough_spec.cr",
"chars": 424,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#query\" do\n it \"calls query against the db driver\" do\n Parent.clear\n Paren"
},
{
"path": "spec/granite/querying/query_builder_spec.cr",
"chars": 4058,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::Query::BuilderMethods do\n describe \"#where\" do\n describe \"with array "
},
{
"path": "spec/granite/querying/reload_spec.cr",
"chars": 490,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#reload\" do\n before_each do\n Parent.clear\n end\n\n it \"reloads the record fro"
},
{
"path": "spec/granite/select/select_spec.cr",
"chars": 1826,
"preview": "require \"../../spec_helper\"\n\ndescribe \"custom select\" do\n before_each do\n Article.clear\n Comment.clear\n EventC"
},
{
"path": "spec/granite/table/table_spec.cr",
"chars": 765,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::Table do\n describe \".table_name\" do\n it \"sets the table name to name "
},
{
"path": "spec/granite/transactions/create_spec.cr",
"chars": 1996,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#create\" do\n it \"creates a new object\" do\n parent = Parent.create(name: \"Test"
},
{
"path": "spec/granite/transactions/destroy_spec.cr",
"chars": 2032,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#destroy\" do\n it \"destroys an object\" do\n parent = Parent.new\n parent.name"
},
{
"path": "spec/granite/transactions/import_spec.cr",
"chars": 6560,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#import\" do\n describe \"using the defualt primary key\" do\n context \"with an AU"
},
{
"path": "spec/granite/transactions/save_natural_key_spec.cr",
"chars": 1240,
"preview": "require \"../../spec_helper\"\n\ndescribe \"(Natural Key) #save\" do\n it \"fails when a primary key is not set\" do\n kv = Kv"
},
{
"path": "spec/granite/transactions/save_spec.cr",
"chars": 5005,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#save\" do\n it \"creates a new object\" do\n parent = Parent.new\n parent.name "
},
{
"path": "spec/granite/transactions/touch_spec.cr",
"chars": 1313,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#touch\" do\n it \"should raise on new record\" do\n expect_raises Exception, \"Can"
},
{
"path": "spec/granite/transactions/update_spec.cr",
"chars": 2672,
"preview": "require \"../../spec_helper\"\n\ndescribe \"#update\" do\n it \"updates an object\" do\n parent = Parent.new(name: \"New Parent"
},
{
"path": "spec/granite/validation_helpers/blank_spec.cr",
"chars": 786,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n context \"Blank\" do\n it \"should work for is_blan"
},
{
"path": "spec/granite/validation_helpers/choice_spec.cr",
"chars": 924,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n context \"Choice\" do\n it \"should work for is_val"
},
{
"path": "spec/granite/validation_helpers/exclusion_spec.cr",
"chars": 608,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n context \"Exclusion\" do\n it \"should allow non re"
},
{
"path": "spec/granite/validation_helpers/inequality_spec.cr",
"chars": 1920,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n context \"Less Than\" do\n it \"should work for les"
},
{
"path": "spec/granite/validation_helpers/lenght_spec.cr",
"chars": 536,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n context \"Length\" do\n it \"should work for length"
},
{
"path": "spec/granite/validation_helpers/nil_spec.cr",
"chars": 1135,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n context \"Nil\" do\n it \"should work for is_nil an"
},
{
"path": "spec/granite/validation_helpers/uniqueness_spec.cr",
"chars": 1073,
"preview": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n context \"Uniqueness\" do\n before_each do\n V"
},
{
"path": "spec/granite/validations/validator_spec.cr",
"chars": 2477,
"preview": "require \"../../spec_helper\"\n\n{% begin %}\n {% adapter_literal = env(\"CURRENT_ADAPTER\").id %}\n class NameTest < Granite:"
},
{
"path": "spec/granite_spec.cr",
"chars": 14240,
"preview": "require \"./spec_helper\"\n\nclass SomeClass\n def initialize(@model_class : Granite::Base.class); end\n\n def valid? : Bool\n"
},
{
"path": "spec/mocks/db_mock.cr",
"chars": 2048,
"preview": "class FakeStatement < DB::Statement\n protected def perform_query(args : Enumerable) : DB::ResultSet\n FieldEmitter.ne"
},
{
"path": "spec/run_all_specs.sh",
"chars": 661,
"preview": "#! /bin/bash\n\nsource .env\n\necho \"Testing PG\"\n\ndocker-compose -f docker/docker-compose.pg.yml build spec\ndocker-compose -"
},
{
"path": "spec/run_test_dbs.sh",
"chars": 442,
"preview": "#!/bin/bash\n\nMYSQL_VERSION=${MYSQL_VERSION:-5.7}\nPG_VERSION=${PG_VERSION:-15.2}\n\ndocker run --name mysql -d \\\n -e MYSQL"
},
{
"path": "spec/spec_helper.cr",
"chars": 1905,
"preview": "require \"mysql\"\nrequire \"pg\"\nrequire \"sqlite3\"\n\nCURRENT_ADAPTER = ENV[\"CURRENT_ADAPTER\"]\nADAPTER_URL = ENV[\""
},
{
"path": "spec/spec_models.cr",
"chars": 19190,
"preview": "require \"uuid\"\n\nclass Granite::Base\n def self.drop_and_create\n end\nend\n\n{% begin %}\n {% adapter_literal = env(\"CURREN"
},
{
"path": "src/adapter/base.cr",
"chars": 5336,
"preview": "require \"../granite\"\nrequire \"db\"\nrequire \"colorize\"\n\n# The Base Adapter specifies the interface that will be used by th"
},
{
"path": "src/adapter/mysql.cr",
"chars": 3550,
"preview": "require \"./base\"\nrequire \"mysql\"\n\n# Mysql implementation of the Adapter\nclass Granite::Adapter::Mysql < Granite::Adapter"
},
{
"path": "src/adapter/pg.cr",
"chars": 4733,
"preview": "require \"./base\"\nrequire \"pg\"\n\n# PostgreSQL implementation of the Adapter\nclass Granite::Adapter::Pg < Granite::Adapter:"
},
{
"path": "src/adapter/sqlite.cr",
"chars": 3197,
"preview": "require \"./base\"\nrequire \"sqlite3\"\n\n# Sqlite implementation of the Adapter\nclass Granite::Adapter::Sqlite < Granite::Ada"
},
{
"path": "src/granite/association_collection.cr",
"chars": 1290,
"preview": "class Granite::AssociationCollection(Owner, Target)\n forward_missing_to all\n\n def initialize(@owner : Owner, @foreign_"
},
{
"path": "src/granite/associations.cr",
"chars": 3540,
"preview": "module Granite::Associations\n macro belongs_to(model, **options)\n {% if model.is_a? TypeDeclaration %}\n {% meth"
},
{
"path": "src/granite/base.cr",
"chars": 2290,
"preview": "require \"./collection\"\nrequire \"./association_collection\"\nrequire \"./associations\"\nrequire \"./callbacks\"\nrequire \"./colu"
},
{
"path": "src/granite/callbacks.cr",
"chars": 1314,
"preview": "module Granite::Callbacks\n class Abort < Exception\n end\n\n CALLBACK_NAMES = %w(before_save after_save before_create af"
},
{
"path": "src/granite/collection.cr",
"chars": 348,
"preview": "class Granite::Collection(M)\n forward_missing_to collection\n\n def initialize(@loader : -> Array(M))\n @loaded = fals"
},
{
"path": "src/granite/columns.cr",
"chars": 7401,
"preview": "require \"json\"\nrequire \"uuid\"\n\nmodule Granite::Columns\n alias SupportedArrayTypes = Array(String) | Array(Int16) | Arra"
},
{
"path": "src/granite/connection_management.cr",
"chars": 2554,
"preview": "module Granite::ConnectionManagement\n macro included\n # Default value for the time a model waits before using a read"
},
{
"path": "src/granite/connections.cr",
"chars": 2123,
"preview": "module Granite\n class Connections\n class_property connection_switch_wait_period : Int32 = 2000\n class_getter regi"
},
{
"path": "src/granite/converters.cr",
"chars": 3043,
"preview": "module Granite::Converters\n # Converts a `UUID` to/from a database column of type `T`.\n #\n # Valid types for `T` incl"
},
{
"path": "src/granite/error.cr",
"chars": 478,
"preview": "class Granite::Error\n property field, message\n\n def initialize(@field : (String | Symbol | JSON::Any), @message : Stri"
},
{
"path": "src/granite/exceptions.cr",
"chars": 478,
"preview": "module Granite\n class RecordNotSaved < ::Exception\n getter model : Granite::Base\n\n def initialize(class_name : St"
},
{
"path": "src/granite/integrators.cr",
"chars": 290,
"preview": "require \"./transactions\"\nrequire \"./querying\"\n\nmodule Granite::Integrators\n include Transactions::ClassMethods\n includ"
},
{
"path": "src/granite/migrator.cr",
"chars": 2743,
"preview": "require \"./error\"\n\n# DB migration tool that prepares a table for the class\n#\n# ```\n# class User < Granite::Base\n# adap"
},
{
"path": "src/granite/query/assemblers/base.cr",
"chars": 5848,
"preview": "module Granite::Query::Assembler\n abstract class Base(Model)\n @placeholder : String = \"\"\n @where : String?\n @o"
},
{
"path": "src/granite/query/assemblers/mysql.cr",
"chars": 331,
"preview": "# Query runner which finalizes a query and runs it.\n# This will likely require adapter specific subclassing :[.\nmodule G"
},
{
"path": "src/granite/query/assemblers/pg.cr",
"chars": 356,
"preview": "# Query runner which finalizes a query and runs it.\n# This will likely require adapter specific subclassing :[.\nmodule G"
},
{
"path": "src/granite/query/assemblers/sqlite.cr",
"chars": 220,
"preview": "module Granite::Query::Assembler\n class Sqlite(Model) < Base(Model)\n @placeholder = \"?\"\n\n def add_parameter(value"
},
{
"path": "src/granite/query/builder.cr",
"chars": 5452,
"preview": "# Data structure which will allow chaining of query components,\n# nesting of boolean logic, etc.\n#\n# Should return self,"
},
{
"path": "src/granite/query/builder_methods.cr",
"chars": 476,
"preview": "module Granite::Query::BuilderMethods\n def __builder\n db_type = case adapter.class.to_s\n when \"Granite:"
},
{
"path": "src/granite/query/executors/base.cr",
"chars": 190,
"preview": "module Granite::Query::Executor\n module Shared\n def raw_sql : String\n @sql\n end\n\n def log(*messages)\n "
},
{
"path": "src/granite/query/executors/list.cr",
"chars": 554,
"preview": "module Granite::Query::Executor\n class List(Model)\n include Shared\n\n def initialize(@sql : String, @args = [] of "
},
{
"path": "src/granite/query/executors/multi_value.cr",
"chars": 630,
"preview": "module Granite::Query::Executor\n class MultiValue(Model, Scalar)\n include Shared\n\n def initialize(@sql : String, "
},
{
"path": "src/granite/query/executors/value.cr",
"chars": 722,
"preview": "module Granite::Query::Executor\n class Value(Model, Scalar)\n include Shared\n\n def initialize(@sql : String, @args"
},
{
"path": "src/granite/querying.cr",
"chars": 5869,
"preview": "module Granite::Querying\n class NotFound < Exception\n end\n\n module ClassMethods\n # Entrypoint for creating a new o"
},
{
"path": "src/granite/select.cr",
"chars": 356,
"preview": "module Granite::Select\n struct Container\n property custom : String?\n getter table_name, fields\n\n def initializ"
},
{
"path": "src/granite/settings.cr",
"chars": 292,
"preview": "module Granite\n class Settings\n property default_timezone : Time::Location = Time::Location.load(Granite::TIME_ZONE)"
},
{
"path": "src/granite/table.cr",
"chars": 1467,
"preview": "# Adds a :nodoc: to granite methods/constants if `DISABLE_GRANTE_DOCS` ENV var is true\nmacro disable_granite_docs?(stmt)"
},
{
"path": "src/granite/transactions.cr",
"chars": 13484,
"preview": "require \"./exceptions\"\n\nmodule Granite::Transactions\n module ClassMethods\n # Removes all records from a table.\n d"
},
{
"path": "src/granite/type.cr",
"chars": 2285,
"preview": "module Granite::Type\n extend self\n\n # :nodoc:\n PRIMITIVES = {\n Int8 => \".read\",\n Int16 => \".read\",\n Int"
},
{
"path": "src/granite/validation_helpers/blank.cr",
"chars": 359,
"preview": "module Granite::ValidationHelpers\n macro validate_not_blank(field)\n validate {{field}}, \"#{{{field}}} must not be bl"
},
{
"path": "src/granite/validation_helpers/choice.cr",
"chars": 274,
"preview": "module Granite::ValidationHelpers\n macro validate_is_valid_choice(field, choices)\n validate {{field}}, \"#{{{field}}}"
},
{
"path": "src/granite/validation_helpers/exclusion.cr",
"chars": 302,
"preview": "module Granite::ValidationHelpers\n macro validate_exclusion(field, excluded_values)\n validate {{field}}, \"#{{{field."
},
{
"path": "src/granite/validation_helpers/inequality.cr",
"chars": 655,
"preview": "module Granite::ValidationHelpers\n macro validate_greater_than(field, amount, or_equal_to = false)\n validate {{field"
},
{
"path": "src/granite/validation_helpers/length.cr",
"chars": 481,
"preview": "module Granite::ValidationHelpers\n macro validate_min_length(field, length)\n validate {{field}}, \"#{{{field}}} is to"
},
{
"path": "src/granite/validation_helpers/nil.cr",
"chars": 337,
"preview": "module Granite::ValidationHelpers\n macro validate_not_nil(field)\n validate {{field}}, \"#{{{field}}} must not be nil\""
},
{
"path": "src/granite/validation_helpers/uniqueness.cr",
"chars": 320,
"preview": "module Granite::ValidationHelpers\n macro validate_uniqueness(field)\n validate {{field}}, \"#{{{field}}} should be uni"
},
{
"path": "src/granite/validators.cr",
"chars": 1900,
"preview": "require \"./error\"\n\n# Analyze validation blocks and procs\n#\n# By example:\n# ```\n# validate :name, \"can't be blank\" do |us"
},
{
"path": "src/granite/version.cr",
"chars": 81,
"preview": "module Granite\n VERSION = {{ `shards version #{__DIR__}`.strip.stringify }}\nend\n"
},
{
"path": "src/granite.cr",
"chars": 322,
"preview": "require \"yaml\"\nrequire \"db\"\nrequire \"log\"\n\nmodule Granite\n Log = ::Log.for(\"granite\")\n\n TIME_ZONE = \"UTC\"\n DATE"
}
]
About this extraction
This page contains the full source code of the amberframework/granite GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 132 files (282.1 KB), approximately 77.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.