Full Code of amberframework/granite for AI

master f4711b63bacf cached
132 files
282.1 KB
77.8k tokens
1 requests
Download .txt
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

Download .txt
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.

Copied to clipboard!