[
  {
    "path": ".dockerignore",
    "content": "bin\n"
  },
  {
    "path": ".envrc",
    "content": "dotenv_if_exists\n"
  },
  {
    "path": ".github/workflows/spec.yml",
    "content": "name: spec\non:\n  push:\n  pull_request:\n    branches: [main, master]\n  # schedule:\n  #   - cron: \"0 6 * * 6\" # Every Saturday 6 AM\njobs:\n  formatting:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Download source\n        uses: actions/checkout@v2\n      - name: Install Crystal\n        uses: oprypin/install-crystal@v1.8.0\n        with:\n          crystal: latest\n      - name: Install Ameba\n        run: shards install\n      - name: Check formatting\n        run: crystal tool format --check\n      - name: Check linting\n        run: ./bin/ameba \n  sqlite-spec:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      max-parallel: 1\n      matrix:\n        crystal: [1.6.2, 1.7.2, 1.8.1, latest]\n    steps:\n      - name: Download source\n        uses: actions/checkout@v2\n      - name: Install Crystal\n        uses: oprypin/install-crystal@v1.8.0\n        with:\n          crystal: ${{ matrix.crystal }}\n      - name: Install shards\n        run: shards update --ignore-crystal-version --skip-postinstall --skip-executables\n      - name: Run tests\n        timeout-minutes: 2\n        run: crystal spec\n        env:\n          CURRENT_ADAPTER: sqlite\n          SQLITE_DATABASE_URL: sqlite3:./granite.db\n          SQLITE_REPLICA_URL: sqlite3:./granite_replica.db\n  mysql-spec:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      max-parallel: 1\n      matrix:\n        crystal: [1.6.2, 1.7.2, 1.8.1, latest]\n    services:\n      mysql:\n        image: mysql:5.7\n        options: >-\n          --health-cmd \"mysqladmin ping\"\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        env:\n          MYSQL_ROOT_PASSWORD: password\n          MYSQL_DATABASE: granite_db\n          MYSQL_USER: granite\n          MYSQL_PASSWORD: password\n        ports:\n          - 3306:3306\n    steps:\n      - name: Download source\n        uses: actions/checkout@v2\n      - name: Install Crystal\n        uses: oprypin/install-crystal@v1.8.0\n        with:\n          crystal: ${{ matrix.crystal }}\n      - name: Install shards\n        run: shards update --ignore-crystal-version --skip-postinstall --skip-executables\n      - name: Run tests\n        timeout-minutes: 2\n        run: crystal spec\n        env:\n          CURRENT_ADAPTER: mysql\n          SQLITE_DATABASE_URL: sqlite3:./granite.db\n          MYSQL_DATABASE_URL: mysql://granite:password@localhost:3306/granite_db\n          MYSQL_REPLICA_URL: mysql://granite:password@localhost:3306/granite_db\n  psql-spec:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      max-parallel: 1\n      matrix:\n        crystal: [1.6.2, 1.7.2, 1.8.1, latest]\n    services:\n      postgres:\n        image: postgres:15.2\n        env:\n          POSTGRES_USER: granite\n          POSTGRES_PASSWORD: password\n          POSTGRES_DB: granite_db\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          # Maps tcp port 5432 on service container to the host\n          - 5432:5432\n    steps:\n      - name: Download source\n        uses: actions/checkout@v2\n      - name: Install Crystal\n        uses: oprypin/install-crystal@v1.8.0\n        with:\n          crystal: ${{ matrix.crystal }}\n      - name: Install shards\n        run: shards update --ignore-crystal-version --skip-postinstall --skip-executables\n      - name: Run tests\n        timeout-minutes: 2\n        run: crystal spec\n        env:\n          CURRENT_ADAPTER: pg\n          PG_DATABASE_URL: postgres://granite:password@localhost:5432/granite_db\n          PG_REPLICA_URL: postgres://granite:password@localhost:5432/granite_db\n"
  },
  {
    "path": ".gitignore",
    "content": "/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 application that uses them\n/.deps.lock\nshard.lock\n\n# Ignore bin because they will be build with shards install\nbin\n.env\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: generic\n\nservices:\n  - docker\n\nbefore_install:\n  - docker-compose -f docker/docker-compose.$CURRENT_ADAPTER.yml build spec\n\nscript:\n  - docker-compose -f docker/docker-compose.$CURRENT_ADAPTER.yml run spec\n\nmatrix:\n  include:\n    - name: \"Mysql 5.7\"\n      env:\n        - CURRENT_ADAPTER=mysql\n        - MYSQL_VERSION=5.7\n        - PG_VERSION=10.5\n    - name: \"Mysql 5.6\"\n      env:\n        - CURRENT_ADAPTER=mysql\n        - MYSQL_VERSION=5.6\n        - PG_VERSION=10.5\n    - name: \"Postgres 9.6\"\n      env:\n        - CURRENT_ADAPTER=pg\n        - MYSQL_VERSION=5.7\n        - PG_VERSION=9.6\n    - name: \"Postgres 10.5\"\n      env:\n        - CURRENT_ADAPTER=pg\n        - MYSQL_VERSION=5.7\n        - PG_VERSION=10.5\n    - name: \"Postgres 11\"\n      env:\n        - CURRENT_ADAPTER=pg\n        - MYSQL_VERSION=5.7\n        - PG_VERSION=11\n    - name: \"Sqlite 3.11.0\"\n      env:\n        - CURRENT_ADAPTER=sqlite\n        - MYSQL_VERSION=5.7\n        - PG_VERSION=10.5\n        - SQLITE_VERSION=3110000\n        - SQLITE_VERSION_YEAR=2016\n    - name: \"Sqlite 3.25.2\"\n      env:\n        - CURRENT_ADAPTER=sqlite\n        - MYSQL_VERSION=5.7\n        - PG_VERSION=10.5\n        - SQLITE_VERSION=3250200\n        - SQLITE_VERSION_YEAR=2018\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM 84codes/crystal:latest-ubuntu-jammy\n\n# Install deps\nRUN apt-get update -qq && apt-get install -y --no-install-recommends libpq-dev libmysqlclient-dev libsqlite3-dev\n\nWORKDIR /app/user\n\nCOPY shard.yml /app/user\nCOPY shard.lock /app/user\nRUN shards install\n\nCOPY src /app/user/src\nCOPY spec /app/user/spec\n\nENTRYPOINT []\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2019 dru.jensen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Granite\n\n[Amber](https://github.com/amberframework/amber) is a web framework written in\nthe [Crystal](https://github.com/crystal-lang/crystal) language.\n\nThis project is to provide an ORM in Crystal.\n\n# Looking for maintainers\n\nGranite 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)\n\n## Documentation\n\n[Documentation](docs/readme.md)\n\n## Contributing\n\n1. Fork it ( https://github.com/amberframework/granite/fork )\n2. Create your feature branch (git checkout -b my-new-feature)\n3. Commit your changes (git commit -am 'Add some feature')\n4. Push to the branch (git push origin my-new-feature)\n5. Create a new Pull Request\n\n## Running tests\nGranite uses Crystal's built in test framework. The tests can be run either within a [dockerized testing environment](#docker-setup) or [locally](#local-setup). \n\nThe test suite depends on access to a PostgreSQL, MySQL, and SQLite database to ensure the adapters work as intended.\n\n### Docker setup\n\nThere is a self-contained testing environment provided via the `docker-compose.yml` file in this repository.\nWe are testing against multiple databases so you have to specify which docker-compose file you would like to use.\n\n- You can find postgres versions at https://hub.docker.com/_/postgres/\n- You can find mysql versions at https://hub.docker.com/_/mysql/\n\nAfter you have docker installed do the following to run tests:\n#### Environment variable setup\n##### Option 1\nExport `.env` with `$ source ./export.sh` or `$ source .env`.\n\n##### Option 2\nModify 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`.\n\n#### First run\n> Replace \"{database_type}\" with \"mysql\" or \"pg\" or \"sqlite\". \n\n```\n$ docker-compose -f docker/docker-compose.{database_type}.yml build spec\n$ docker-compose -f docker/docker-compose.{database_type}.yml run spec\n```\n\n#### Subsequent runs\n\n```\n$ docker-compose -f docker/docker-compose.{database_type}.yml run spec\n```\n\n#### Cleanup\n\nIf you're done testing and you'd like to shut down and clean up the docker dependences run the following:\n\n```\n$ docker-compose -f docker/docker-compose.{database_type}.yml down\n```\n\n#### Run all\n\nTo run the specs for each database adapter use `./spec/run_all_specs.sh`.    This will build and run each adapter, then cleanup after itself.\n\n### Local setup\n\nIf you'd like to test without docker you can do so by following the instructions below:\n\n1. Install dependencies with `$ shards install `\n2. Update .env to use appropriate ENV variables, or create appropriate databases.\n3. Setup databases:\n\n#### PostgreSQL\n\n```sql\nCREATE USER granite WITH PASSWORD 'password';\n\nCREATE DATABASE granite_db;\n\nGRANT ALL PRIVILEGES ON DATABASE granite_db TO granite;\n```\n\n#### MySQL\n\n```sql\nCREATE USER 'granite'@'localhost' IDENTIFIED BY 'password';\n\nCREATE DATABASE granite_db;\n\nGRANT ALL PRIVILEGES ON granite_db.* TO 'granite'@'localhost' WITH GRANT OPTION;\n```\n\n4. Export `.env` with `$ source ./export.sh` or `$ source .env`.\n5. `$ crystal spec`\n"
  },
  {
    "path": "docker/docker-compose.mysql.yml",
    "content": "version: '2'\nservices:\n  spec:\n    extends:\n      file: ../docker-compose.yml\n      service: spec\n    environment:\n      CURRENT_ADAPTER: mysql\n    depends_on:\n      - mysql\n  mysql:\n    image: mysql:${MYSQL_VERSION}\n    environment:\n      MYSQL_ROOT_PASSWORD: pass\n      MYSQL_DATABASE: test\n      MYSQL_USER: user\n      MYSQL_PASSWORD: pass\n"
  },
  {
    "path": "docker/docker-compose.pg.yml",
    "content": "version: '2'\nservices:\n  spec:\n    extends:\n      file: ../docker-compose.yml\n      service: spec\n    environment:\n      CURRENT_ADAPTER: pg\n    depends_on:\n      - pg\n  pg:\n    image: postgres:${PG_VERSION}\n    environment:\n      POSTGRES_PASSWORD: pass\n"
  },
  {
    "path": "docker/docker-compose.sqlite.yml",
    "content": "version: '2'\nservices:\n  spec:\n    extends:\n      file: ../docker-compose.yml\n      service: spec\n    build:\n      context: ../\n    environment:\n      CURRENT_ADAPTER: sqlite\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '2'\nservices:\n  spec:\n    build: .\n    command: 'bash -c \"cd /app/user && bin/ameba && crystal tool format --check && crystal spec --warnings all\"'\n    working_dir: /app/user\n    environment:\n      PG_DATABASE_URL: 'postgres://postgres:pass@pg:5432/postgres'\n      MYSQL_DATABASE_URL: 'mysql://user:pass@mysql:3306/test'\n      SQLITE_DATABASE_URL: 'sqlite3:./test.db'\n      CURRENT_ADAPTER: sqlite\n"
  },
  {
    "path": "docs/callbacks.md",
    "content": "# Callbacks\n\nCall a specified method on a specific life cycle event.\n\nHere is an example:\n\n```crystal\nrequire \"granite/adapter/pg\"\n\nclass Post < Granite::Base\n  connection pg\n\n  before_save :upcase_title\n\n  column id : Int64, primary: true\n  column title : String\n  column content : String\n  timestamps\n\n  def upcase_title\n    if title = @title\n      @title = title.upcase\n    end\n  end\nend\n```\n\nYou can register callbacks for the following events:\n\n## Create\n\n- before_save\n- before_create\n- **save**\n- after_create\n- after_save\n\n## Update\n\n- before_save\n- before_update\n- **save**\n- after_update\n- after_save\n\n## Destroy\n\n- before_destroy\n- **destroy**\n- after_destroy\n"
  },
  {
    "path": "docs/crud.md",
    "content": "# CRUD\n\n## Create\n\nCombination of object creation and insertion into database.\n\n```crystal\nPost.create(name: \"Granite Rocks!\", body: \"Check this out.\") # Set attributes and call save\nPost.create!(name: \"Granite Rocks!\", body: \"Check this out.\") # Set attributes and call save!. Will throw an exception when the save failed\n```\n\nTo create a record without setting the `created_at` & `updated_at` fields, you can pass in `skip_timestamps`.\n\n```crystal\nPost.create({name: \"Granite Rocks!\", body: \"Check this out.\"}, skip_timestamps: true)\n```\n\n## Insert\n\nInserts an already created object into the database.\n\n```crystal\npost = Post.new\npost.name = \"Granite Rocks!\"\npost.body = \"Check this out.\"\npost.save\n\npost = Post.new\npost.name = \"Granite Rocks!\"\npost.body = \"Check this out.\"\npost.save! # raises when save failed\n```\n\nTo skip the validation callbacks, pass in `validate: false`:\n\n```crystal\npost.save(validate: false)\npost.save!(validate: false)\n```\n\nYou can also pass in `skip_timestamps` to save without changing the `updated_at` field on update:\n\n```crystal\npost.save(skip_timestamps: true)\npost.save!(skip_timestamps: true)\n```\n\n## Read\n\n### find\n\nFinds the record with the given primary key.\n\n```crystal\npost = Post.find 1\nif post\n  puts post.name\nend\n\npost = Post.find! 1 # raises when no records found\n```\n\n### find_by\n\nFinds the record(s) that match the given criteria\n\n```crystal\npost = Post.find_by(slug: \"example_slug\")\nif post\n  puts post.name\nend\n\npost = Post.find_by!(slug: \"foo\") # raises when no records found.\nother_post = Post.find_by(slug: \"foo\", type: \"bar\") # Also works for multiple arguments.\n```\n\n### first\n\nReturns the first record.\n\n```crystal\npost = Post.first\nif post\n  puts post.name\nend\n\npost = Post.first! # raises when no records exist\n```\n\n### reload\n\nReturns the record with the attributes reloaded from the database.\n\n**Note:** this method is only defined when the `Spec` module is present.\n\n```\npost = Post.create(name: \"Granite Rocks!\", body: \"Check this out.\")\n# record gets updated by another process\npost.reload # performs another find to fetch the record again\n```\n\n### where, order, limit, offset, group_by\n\nSee [querying](./querying.md) for more details of using the QueryBuilder.\n\n### all\n\nReturns all records of a model.\n\n```crystal\nposts = Post.all\nif posts\n  posts.each do |post|\n    puts post.name\n  end\nend\n```\n\nSee [querying](./querying.md#all) for more details on using `all`\n\n## Update\n\nUpdates a given record already saved in the database.\n\n```crystal\npost = Post.find 1\npost.name = \"Granite Really Rocks!\"\npost.save\n\npost = Post.find 1\npost.update(name: \"Granite Really Rocks!\") # Assigns attributes and calls save\n\npost = Post.find 1\npost.update!(name: \"Granite Really Rocks!\") # Assigns attributes and calls save!. Will throw an exception when the save failed\n```\n\nTo update a record without changing the `updated_at` field, you can pass in `skip_timestamps`:\n\n```crystal\npost = Post.find 1\npost.update({name: \"Granite Really Rocks!\"}, skip_timestamps: true)\npost.update!({name: \"Granite Really Rocks!\"}, skip_timestamps: true)\n```\n\n## Delete\n\nDelete a specific record.\n\n```crystal\npost = Post.find 1\npost.destroy if post\nputs \"deleted\" if post.destroyed?\n\npost = Post.find 1\npost.destroy! # raises when delete failed\n```\n\nClear all records of a model\n\n```crystal\nPost.clear #truncate the table\n```\n"
  },
  {
    "path": "docs/imports.md",
    "content": "# Bulk Insertions\n\n## Import\n\n> **Note:** Imports do not trigger callbacks automatically. See [Running Callbacks](#running-callbacks).\n\nEach model has an `.import` method that will save an array of models in one bulk insert statement.\n\n```Crystal\nmodels = [\n  Model.new(id: 1, name: \"Fred\", age: 14),\n  Model.new(id: 2, name: \"Joe\", age: 25),\n  Model.new(id: 3, name: \"John\", age: 30),\n]\n\nModel.import(models)\n```\n\n## update_on_duplicate\n\nThe `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.\n\n```Crystal\nmodels = [\n  Model.new(id: 1, name: \"Fred\", age: 14),\n  Model.new(id: 2, name: \"Joe\", age: 25),\n  Model.new(id: 3, name: \"John\", age: 30),\n]\n\nModel.import(models)\n\nModel.find!(1).name # => Fred\n\nmodels = [\n  Model.new(id: 1, name: \"George\", age: 14),\n]\n\nModel.import(models, update_on_duplicate: true, columns: %w(name))\n\nModel.find!(1).name # => George\n```\n\n**NOTE: If using PostgreSQL you must have version 9.5+ to have the on_duplicate_key_update feature.**\n\n## ignore_on_duplicate\n\nThe `import` method has an optional `ignore_on_duplicate` param, that takes a boolean, which will skip records if the primary constraint is violated.\n\n```Crystal\nmodels = [\n  Model.new(id: 1, name: \"Fred\", age: 14),\n  Model.new(id: 2, name: \"Joe\", age: 25),\n  Model.new(id: 3, name: \"John\", age: 30),\n]\n\nModel.import(models)\n\nModel.find!(1).name # => Fred\n\nmodels = [\n  Model.new(id: 1, name: \"George\", age: 14),\n]\n\nModel.import(models, ignore_on_duplicate: true)\n\nModel.find!(1).name # => Fred\n```\n\n## batch_size\n\nThe `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.\n\n```Crystal\nmodels = [\n  Model.new(id: 1, name: \"Fred\", age: 14),\n  Model.new(id: 2, name: \"Joe\", age: 25),\n  Model.new(id: 3, name: \"John\", age: 30),\n  Model.new(id: 3, name: \"Bill\", age: 66),\n]\n\nModel.import(models, batch_size: 2)\n# => First SQL INSERT statement imports Fred and Joe\n# => Second SQL INSERT statement imports John and Bill\n```\n\n## Running Callbacks\n\nSince 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:\n\n```Crystal\nrequire \"uuid\"\n\nclass Item < Granite::Base\n  connection mysql\n  table items\n\n  column item_id : String, primary: true, auto: false\n  column item_name : String\n\n  before_create :generate_uuid\n\n  def generate_uuid\n    @item_id = UUID.random.to_s\n  end\nend\n```\n\n```Crystal\nitems = [\n  Item.new(item_name: \"item1\"),\n  Item.new(item_name: \"item2\"),\n  Item.new(item_name: \"item3\"),\n  Item.new(item_name: \"item4\"),\n]\n\n# 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.\n\n# Manually run the callback on each model to generate the item_id.\nitems.each(&.before_create)\n\n# Each model in the array now has a item_id set, so can be imported.\nItem.import(items)\n\n# This can also be used for a single record.\nitem = Item.new(item_name: \"item5\")\nitem.before_create\nitem.save\n```\n\n> **Note:** Manually running your callbacks is mainly aimed at bulk imports. Running them before a normal `.save`, for example, would run your callbacks twice.\n"
  },
  {
    "path": "docs/migrations.md",
    "content": "# Migrations\n\n## Database Migrations with micrate\n\nIf 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.\n\n### Install\n\nAdd micrate your shards.yml\n\n```yaml\ndependencies:\n  micrate:\n    github: juanedi/micrate\n```\n\nUpdate shards\n\n```sh\n$ shards update\n```\n\nCreate 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`.\n\n```crystal\n#! /usr/bin/env crystal\n#\n# To build a standalone command line client, require the\n# driver you wish to use and use `Micrate::Cli`.\n#\n\nrequire \"micrate\"\nrequire \"pg\"\n\nMicrate::DB.connection_url = ENV[\"DATABASE_URL\"]\nMicrate::Cli.run\n```\n\nMake it executable:\n\n```sh\n$ chmod +x bin/micrate\n```\n\nWe should now be able to run micrate commands.\n\n`$ bin/micrate help` => should output help commands.\n\n### Creating a migration\n\nLet's create a `posts` table in our database.\n\n```sh\n$ bin/micrate scaffold create_posts\n```\n\nThis will create a file under `db/migrations`. Let's open it and define our posts schema.\n\n```sql\n-- +micrate Up\n-- SQL in section 'Up' is executed when this migration is applied\nCREATE TABLE posts(\n  id BIGSERIAL PRIMARY KEY,\n  title VARCHAR NOT NULL,\n  body TEXT NOT NULL,\n  created_at TIMESTAMP,\n  updated_at TIMESTAMP\n);\n\n-- +micrate Down\n-- SQL section 'Down' is executed when this migration is rolled back\nDROP TABLE posts;\n```\n\nAnd now let's run the migration\n\n```sh\n$ bin/micrate up\n```\n\nYou should now have a `posts` table in your database ready to query.\n"
  },
  {
    "path": "docs/models.md",
    "content": "# Model Usage\n\n## Multiple Connections\n\nIt is possible to register multiple connections, for example:\n\n```crystal\nGranite::Connections << Granite::Adapter::Mysql.new(name: \"legacy_db\", url: \"LEGACY_DB_URL\")\nGranite::Connections << Granite::Adapter::Pg.new(name: \"new_db\", url: \"NEW_DB_URL\")\n\nclass Foo < Granite::Base\n  connection legacy_db\n\n  # model fields\nend\n\nclass Bar < Granite::Base\n  connection new_db\n\n  # model fields\nend\n```\n\nIn 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.\n\n> **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`.\n\n## timestamps\n\nThe `timestamps` macro defines `created_at` and `updated_at` field for you.\n\n```crystal\nclass Bar < Granite::Base\n  connection mysql\n\n  # Other fields\n  timestamps\nend\n```\n\nWould be equivalent to:\n\n```crystal\nclass Bar < Granite::Base\n  connection mysql\n\n  column created_at : Time?\n  column updated_at : Time?\nend\n```\n\n## Primary Keys\n\nEach model is required to have a primary key defined. Use the `column` macro with the `primary: true` option to denote the primary key.\n\n> **NOTE:** Composite primary keys are not yet supported.\n\n```crystal\nclass Site < Granite::Base\n  connection mysql\n\n  column id : Int64, primary: true\n  column name : String\nend\n```\n\n`belongs_to` associations can also be used as a primary key in much the same way.\n\n```crystal\nclass ChatSettings < Granite::Base\n  connection mysql\n\n  # chat_id would be the primary key\n  belongs_to chat : Chat, primary: true\nend\n```\n\n### Custom\n\nThe name and type of the primary key can also be changed from the recommended `id : Int64`.\n\n```crystal\nclass Site < Granite::Base\n  connection mysql\n\n  column custom_id : Int32, primary: true\n  column name : String\nend\n```\n\n### Natural Keys\n\nPrimary keys are defined as auto incrementing by default. For natural keys, you can set `auto: false` option.\n\n```crystal\nclass Site < Granite::Base\n  connection mysql\n\n  column custom_id : Int32, primary: true, auto: false\n  column name : String\nend\n```\n\n### UUIDs\n\nFor 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.\n\n```crystal\nclass Book < Granite::Base\n  connection mysql\n\n  column isbn : UUID, primary: true\n  column name : String\nend\n\nbook = Book.new\nbook.name = \"Moby Dick\"\nbook.isbn # => nil\nbook.save\nbook.isbn # => RFC4122 V4 UUID string\n```\n\n## Default values\n\nA default value can be defined that will be used if another value is not specified/supplied.\n\n```crystal\nclass Book < Granite::Base\n  connection mysql\n\n  column id : Int64, primary: true\n  column name : String = \"DefaultBook\"\nend\n\nbook = Book.new\nbook.name # => \"DefaultBook\"\n```\n\n## Generating Documentation\n\nBy 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`.\n\nDoc block comments can be applied above the `column` macro.\n\n```crystal\n# If the item is public.\ncolumn published : Bool\n```\n\n## Annotations\n\nAnnotations 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.\n\n```crystal\nclass Foo < Granite::Base\n  connection mysql\n  table foos\n\n  column id : Int64, primary: true\n\n  @[JSON::Field(ignore: true)]\n  @[Bar::Settings(other_option: 7)]\n  column password : String\n\n  column name : String\n  column age : Int32\nend\n```\n\n## Converters\n\nGranite 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.\n\nEach 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`.\n\nCurrently Granite supports various converters, each with their own supported database column types:\n\n- `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`.\n- `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`.\n  - **NOTE:** `M` must implement `#to_json` and `.from_json` methods.\n- `PgNumeric` - Converts a `PG::Numeric` value to a `Float64` on read.\n\nThe 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`.\n\n```crystal\nenum OrderStatus\n  Active\n  Expired\n  Completed\nend\n\nclass Order < Granite::Base\n  connection mysql\n  table foos\n\n  # Other fields\n  column status : OrderStatus, converter: Granite::Converters::Enum(OrderStatus, String)\nend\n```\n\n## Serialization\n\nGranite 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.\n"
  },
  {
    "path": "docs/multiple_connections.md",
    "content": "# Read replica support\n\nIn 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:\n\n```crystal\nGranite::Connections << {name: \"psql\", writer: \"...\", reader: \"...\", adapter_type: Granite::Adapter::Pg}\n```\n\nThe 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\n\n```crystal\nclass User < Granite::Base\n  connection \"psql\"\nend\n```\n\nwhere the value provided to the `connection` macro is the name of the granite connection you want to use.\n\nThe `writer` is a connection string to the database node that has read/write access.\n\nThe `reader` is a connection string to the database node that can only be read from.\n\nThe 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.\n\n## configuring the connection switch wait period\n\nBy 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. \n\nThe default value is `2000` milliseconds. You can change this value like this\n\n```crystal\nGranite::Conections.connection_switch_wait_period = 2000 #=> time in milliseconds\n```\n"
  },
  {
    "path": "docs/querying.md",
    "content": "# Querying\n\nThe query macro and where clause combine to give you full control over your query.\n\n## Where\n\nWhere is using a QueryBuilder that allows you to chain where clauses together to build up a complete query.\n\n```crystal\nposts = Post.where(published: true, author_id: User.first!.id)\n```\n\nIt supports different operators:\n\n```crystal\nPost.where(:created_at, :gt, Time.local - 7.days)\n```\n\nSupported operators are :eq, :gteq, :lteq, :neq, :gt, :lt, :nlt, :ngt, :ltgt, :in, :nin, :like, :nlike\n\nAlternatively, `#where`, `#and`, and `#or` accept a raw SQL clause, with an optional placeholder (`?` for MySQL/SQLite, `$` for Postgres) to avoid SQL Injection.\n\n```crystal\n# Example using Postgres adapter\nPost.where(:created_at, :gt, Time.local - 7.days)\n  .where(\"LOWER(author_name) = $\", name)\n  .where(\"tags @> '{\"Journal\", \"Book\"}') # PG's array contains operator\n```\n\nThis 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.**\n\n## Order\n\nOrder is using the QueryBuilder and supports providing an ORDER BY clause:\n\n```crystal\nPost.order(:created_at)\n```\n\nDirection\n\n```crystal\nPost.order(updated_at: :desc)\n```\n\nMultiple fields\n\n```crystal\nPost.order([:created_at, :title])\n```\n\nWith direction\n\n```crystal\nPost.order(created_at: :desc, title: :asc)\n```\n\n## Group By\n\nGroup is using the QueryBuilder and supports providing an GROUP BY clause:\n\n```crystal\nposts = Post.group_by(:published)\n```\n\nMultiple fields\n\n```crystal\nPost.group_by([:published, :author_id])\n```\n\n## Limit\n\nLimit is using the QueryBuilder and provides the ability to limit the number of tuples returned:\n\n```crystal\nPost.limit(50)\n```\n\n## Offset\n\nOffset is using the QueryBuilder and provides the ability to offset the results. This is used for pagination:\n\n```crystal\nPost.offset(100).limit(50)\n```\n\n## All\n\nAll is not using the QueryBuilder. It allows you to directly query the database using SQL.\n\nWhen using the `all` method, the selected fields will match the\nfields specified in the model unless the `select` macro was used to customize\nthe SELECT.\n\nAlways pass in parameters to avoid SQL Injection. Use a `?`\nin your query as placeholder. Checkout the [Crystal DB Driver](https://github.com/crystal-lang/crystal-db)\nfor documentation of the drivers.\n\nHere are some examples:\n\n```crystal\nposts = Post.all(\"WHERE name LIKE ?\", [\"Joe%\"])\nif posts\n  posts.each do |post|\n    puts post.name\n  end\nend\n\n# ORDER BY Example\nposts = Post.all(\"ORDER BY created_at DESC\")\n\n# JOIN Example\nposts = Post.all(\"JOIN comments c ON c.post_id = post.id\n                  WHERE c.name = ?\n                  ORDER BY post.created_at DESC\",\n                  [\"Joe\"])\n```\n\n## Customizing SELECT\n\nThe `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:\n\n```crystal\nclass CustomView < Granite::Base\n  connection pg\n\n  column id : Int64, primary: true\n  column articlebody : String\n  column commentbody : String\n\n  select_statement <<-SQL\n    SELECT articles.articlebody, comments.commentbody\n    FROM articles\n    JOIN comments\n    ON comments.articleid = articles.id\n  SQL\nend\n```\n\nYou can combine this with an argument to `all` or `first` for maximum flexibility:\n\n```crystal\nresults = CustomView.all(\"WHERE articles.author = ?\", [\"Noah\"])\n```\n\nNote - 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.\n\n## Exists?\n\nThe `exists?` class method returns `true` if a record exists in the table that matches the provided _id_ or _criteria_, otherwise `false`.\n\nIf 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`.\n\n```crystal\n# Assume a model named Post with a title field\npost = Post.new(title: \"My Post\")\npost.save\npost.id # => 1\n\nPost.exists? 1 # => true\nPost.exists? {\"id\" => 1, :title => \"My Post\"} # => true\nPost.exists? {id: 1, title: \"Some Post\"} # => false\n```\n\nThe `exists?` method can also be used with the query builder.\n\n```crystal\nPost.where(published: true, author_id: User.first!.id).exists?\nPost.where(:created_at, :gt, Time.local - 7.days).exists?\n```\n"
  },
  {
    "path": "docs/readme.md",
    "content": "# Documentation\n\n## Getting Started\n\n### Installation\n\nAdd this library to your projects dependencies along with the driver in\nyour `shard.yml`. This can be used with any framework but was originally\ndesigned to work with the amber framework in mind. This library will work\nwith Kemal or any other framework as well.\n\n```yaml\ndependencies:\n  granite:\n    github: amberframework/granite\n\n  # Pick your database\n  mysql:\n    github: crystal-lang/crystal-mysql\n\n  sqlite3:\n    github: crystal-lang/crystal-sqlite3\n\n  pg:\n    github: will/crystal-pg\n```\n\n### Register a Connection\n\nNext you will need to register a connection. This should be one of the first things in your main Crystal file, before Granite is required.\n\n```crystal\nGranite::Connections << Granite::Adapter::Mysql.new(name: \"mysql\", url: \"YOUR_DATABASE_URL\")\n\n# Rest of code...\n```\n\nSupported adapters include: `Mysql, Pg, and Sqlite`.\n\n### Example Model\n\nHere is an example Granite model using the connection registered above.\n\n```crystal\nrequire \"granite/adapter/mysql\"\n\nclass Post < Granite::Base\n  connection mysql\n  table posts # Name of the table to use for the model, defaults to class name snake cased\n\n  column id : Int64, primary: true # Primary key, defaults to AUTO INCREMENT\n  column name : String? # Nilable field\n  column body : String # Not nil field\nend\n```\n\n## Additional Documentation\n\n[Models](./models.md)\n\n[CRUD](./crud.md)\n\n[Querying](./querying.md)\n\n[Relationships](./relationships.md)\n\n[Validation](./validations.md)\n\n[Callbacks](./callbacks.md)\n\n[Migrations](./migrations.md)\n\n[Imports](./imports.md)\n"
  },
  {
    "path": "docs/relationships.md",
    "content": "# Relationships\n\n## One to One\n\nFor one-to-one relationships, You can use the `has_one` and `belongs_to` in your models.\n\n> **Note:** one-to-one relationship does not support through associations yet.\n\n```crystal\nclass Team < Granite::Base\n  has_one :coach\n\n  column id : Int64, primary: true\n  column name : String\nend\n```\n\nThis will add a `coach` and `coach=` instance methods to the team which returns associated coach.\n\n```crystal\nclass Coach < Granite::Base\n  table coaches\n\n  belongs_to :team\n\n  column id : Int64, primary: true\n  column name : String\nend\n```\n\nThis will add a `team` and `team=` instance method to the coach.\n\nFor example:\n\n```crystal\nteam = Team.find! 1\n# has_one side..\nputs team.coach\n\ncoach = Coach.find! 1\n# belongs_to side...\nputs coach.team\n\ncoach.team = team\ncoach.save\n\n# or in one-to-one you can also do\n\nteam.coach = coach\n# coach is the child entity and contians the foreign_key\n# so save should called on coach instance\ncoach.save\n\n```\n\nIn this example, you will need to add a `team_id` and index to your coaches table:\n\n```sql\nCREATE TABLE coaches (\n  id BIGSERIAL PRIMARY KEY,\n  team_id BIGINT,\n  name VARCHAR,\n  created_at TIMESTAMP,\n  updated_at TIMESTAMP\n);\n\nCREATE INDEX team_id_idx ON coaches (team_id);\n```\n\nForeign 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:\n\n```crystal\nclass Team < Granite::Base\n  has_one :coach, foreign_key: :custom_id\n\n  column id : Int64, primary: true\n  column name : String\nend\n\nclass Coach < Granite::Base\n  belongs_to :team\n\n  column id : Int64, primary: true\nend\n```\n\nThe class name inferred from the name but you can specify the class name:\n\n```crystal\nclass Team < Granite::Base\n  has_one coach : Coach, foreign_key: :custom_id\n\n  # or you can provide the class name as a parameter\n  has_one :coach, class_name: Coach, foreign_key: :custom_id\n\n  column id : Int64, primary: true\n  column name : String\nend\n\nclass Coach < Granite::Base\n  belongs_to team : Team\n\n  # provide a custom foreign key\n  belongs_to team : Team, foreign_key: team_uuid : String\n\n  column id : Int64, primary: true\nend\n```\n\n## One to Many\n\n`belongs_to` and `has_many` macros provide a rails like mapping between Objects.\n\n```crystal\nclass User < Granite::Base\n  connection mysql\n\n  has_many :post\n\n  # pluralization requires providing the class name\n  has_many posts : Post\n\n  # or you can provide class name as a parameter\n  has_many :posts, class_name: Post\n\n  # you can provide a custom foreign key\n  has_many :posts, class_name: Post, foreign_key: :custom_id\n\n  column id : Int64, primary: true\n  column name : String\n  column email : String\n  timestamps\nend\n```\n\nThis will add a `posts` instance method to the user which returns an array of posts.\n\n```crystal\nclass Post < Granite::Base\n  connection mysql\n  table posts\n\n  belongs_to :user\n\n  # or custom name\n  belongs_to my_user : User\n\n  # or custom foreign key\n  belongs_to user : User, foreign_key: uuid : String\n\n  column id : Int64, primary: true\n  column title : String\n  timestamps\nend\n```\n\nThis will add a `user` and `user=` instance method to the post.\n\nFor example:\n\n```crystal\nuser = User.find! 1\nuser.posts.each do |post|\n  puts post.title\nend\n\npost = Post.find! 1\nputs post.user\n\npost.user = user\npost.save\n```\n\nIn this example, you will need to add a `user_id` and index to your posts table:\n\n```sql\nCREATE TABLE posts (\n  id BIGSERIAL PRIMARY KEY,\n  user_id BIGINT,\n  title VARCHAR,\n  created_at TIMESTAMP,\n  updated_at TIMESTAMP\n);\n\nCREATE INDEX user_id_idx ON posts (user_id);\n```\n\n## Many to Many\n\nInstead 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.\n\nThen you can use the `belongs_to` and `has_many` relationships going both ways.\n\n```crystal\nclass User < Granite::Base\n  has_many :participants, class_name: Participant\n\n  column id : Int64, primary: true\n  column name : String\nend\n\nclass Participant < Granite::Base\n  table participants\n\n  belongs_to :user\n  belongs_to :room\n\n  column id : Int64, primary: true\nend\n\nclass Room < Granite::Base\n  table rooms\n\n  has_many :participants, class_name: Participant\n\n  column id : Int64, primary: true\n  column name : String\nend\n```\n\nThe Participant class represents the many-to-many relationship between the Users and Rooms.\n\nHere is what the database table would look like:\n\n```sql\nCREATE TABLE participants (\n  id BIGSERIAL PRIMARY KEY,\n  user_id BIGINT,\n  room_id BIGINT,\n  created_at TIMESTAMP,\n  updated_at TIMESTAMP\n);\n\nCREATE INDEX user_id_idx ON TABLE participants (user_id);\nCREATE INDEX room_id_idx ON TABLE participants (room_id);\n```\n\n## has_many through:\n\nAs a convenience, we provide a `through:` clause to simplify accessing the many-to-many relationship:\n\n```crystal\nclass User < Granite::Base\n  has_many :participants, class_name: Participant\n  has_many :rooms, class_name: Room, through: :participants\n\n  column id : Int64, primary: true\n  column name : String\nend\n\nclass Participant < Granite::Base\n  belongs_to :user\n  belongs_to :room\n\n  column id : Int64, primary: true\nend\n\nclass Room < Granite::Base\n  has_many :participants, class_name: Participant\n  has_many :users, class_name: User, through: :participants\n\n  column id : Int64, primary: true\n  column name : String\nend\n```\n\nThis will allow you to find all the rooms that a user is in:\n\n```crystal\nuser = User.create(name: \"Bob\")\nroom = Room.create(name: \"#crystal-lang\")\nroom2 = Room.create(name: \"#amber\")\nParticipant.create(user_id: user.id, room_id: room.id)\nParticipant.create(user_id: user.id, room_id: room2.id)\n\nuser.rooms.each do |room|\n  puts room.name\nend\n```\n\nAnd the reverse, all the users in a room:\n\n```crystal\nroom.users.each do |user|\n  puts user.name\nend\n```\n"
  },
  {
    "path": "docs/validations.md",
    "content": "# Errors\n\nAll database errors are added to the `errors` array used by `Granite::Validators` with the symbol `:base`\n\n```crystal\npost = Post.new\npost.save\npost.errors[0].to_s.should eq \"ERROR: name cannot be null\"\n```\n\n## Validations\n\nValidations can be made on models to ensure that given criteria are met.\n\nModels that do not pass the validations will not be saved, and will have the errors added to the model's `errors` array.\n\nFor example, asserting that the title on a post is not blank:\n\n```Crystal\nclass Post < Granite::Base\n  connection mysql\n\n  column id : Int64, primary: true\n  column title : String\n\n  validate :title, \"can't be blank\" do |post|\n    !post.title.to_s.blank?\n  end\nend\n`\n```\n\n## Validation Helpers\n\nA set of common validation macros exist to make validations easier to manage/create.\n\n### Common\n\n- `validate_not_nil :field` - Validates that field should not be nil.\n- `validate_is_nil :field` - Validates that field should be nil.\n- `validate_is_valid_choice :type, [\"allowedType1\", \"allowedType2\"]` - Validates that type should be one of a preset option.\n- `validate_exclusion :type, [\"notAllowedType1\", \"notAllowedType2\"]` - Validates that type should not be one of a preset option.\n- `validate_uniqueness :field` - Validates that the field is unique\n\n### String\n\n- `validate_not_blank :field` - Validates that field should not be blank.\n- `validate_is_blank :field` - Validates that field should be blank.\n- `validate_min_length :field, 5` - Validates that field should be at least 5 long\n- `validate_max_length :field, 20` - Validates that field should be at most 20 long\n\n### String\n\n- `validate_greater_than :field, 0` - Validates that field should be greater than 0.\n- `validate_greater_than :field, 0, true` - Validates that field should be greater than or equal to 0.\n- `validate_less_than :field, 100` - Validates that field should be less than 100.\n- `validate_less_than :field, 100, true` - Validates that field should be less than or equal to 100.\n\nUsing the helpers, the previous example could have been written like:\n\n```Crystal\nclass Post < Granite::Base\n  connection mysql\n\n  column id : Int64, primary: true\n  column title : String\n\n  validate_not_blank :title\nend\n```\n"
  },
  {
    "path": "export.sh",
    "content": "#!/bin/bash\n\nif [ -f .env ]; then\n   while IFS= read -r line; do\n       export \"$line\"\n   done < .env\n   echo \"Environment variables from .env file have been exported.\"\nelse\n   echo \"Error: The .env file does not exist.\"\nfi\n"
  },
  {
    "path": "shard.yml",
    "content": "name: granite\n\nversion: 0.23.4\n\ncrystal: \">= 1.6.0, < 2.0.0\"\n\nauthors:\n  - drujensen <drujensen@gmail.com>\n  - elorest <isaac@isaacsloan.com>\n\nlicense: MIT\n\ndependencies:\n  db:\n    github: crystal-lang/crystal-db\n    version: ~> 0.13.1\n\ndevelopment_dependencies:\n  mysql:\n    github: crystal-lang/crystal-mysql\n    version: ~> 0.16.0\n  sqlite3:\n    github: crystal-lang/crystal-sqlite3\n    version: ~> 0.21.0\n  pg:\n    github: will/crystal-pg\n    version: ~> 0.29.0\n  ameba:\n    github: crystal-ameba/ameba\n    version: ~> 1.5.0\n"
  },
  {
    "path": "spec/adapter/adapters_spec.cr",
    "content": "require \"../spec_helper\"\n\nclass Foo < Granite::Base\n  connection {{env(\"CURRENT_ADAPTER\").id}}\n  column id : Int64, primary: true\nend\n\nclass Bar < Granite::Base\n  column id : Int64, primary: true\nend\n\ndescribe Granite::Connections do\n  describe \"registration\" do\n    it \"should allow connections to be be saved and looked up\" do\n      Granite::Connections.registered_connections.size.should eq 2\n\n      if connection = Granite::Connections[CURRENT_ADAPTER]\n        connection[:writer].url.should eq ADAPTER_URL\n      else\n        connection.should_not be_falsey\n      end\n\n      case ENV[\"CURRENT_ADAPTER\"]?\n      when \"sqlite\"\n        if connection = Granite::Connections[\"sqlite_with_replica\"]\n          connection[:writer].url.should eq ENV[\"SQLITE_DATABASE_URL\"]?\n          connection[:reader].url.should eq ADAPTER_REPLICA_URL\n        else\n          connection.should_not be_falsey\n        end\n      end\n    end\n\n    it \"should disallow multiple connections with the same name\" do\n      Granite::Connections << Granite::Adapter::Pg.new(name: \"mysql2\", url: \"mysql://localhost:3306/test\")\n      expect_raises(Exception, \"Adapter with name 'mysql2' has already been registered.\") do\n        Granite::Connections << Granite::Adapter::Pg.new(name: \"mysql2\", url: \"mysql://localhost:3306/test\")\n      end\n    end\n\n    it \"should assign the correct connections to a model\" do\n      adapter = Foo.adapter\n      adapter.name.should eq CURRENT_ADAPTER\n      adapter.url.should eq ADAPTER_URL\n    end\n\n    it \"should use the first registered connection if none are specified\" do\n      adapter = Bar.adapter\n      adapter.name.should eq CURRENT_ADAPTER\n      adapter.url.should eq ADAPTER_URL\n    end\n  end\nend\n"
  },
  {
    "path": "spec/adapter/mysql_spec.cr",
    "content": "require \"../spec_helper\"\n"
  },
  {
    "path": "spec/adapter/pg_spec.cr",
    "content": "require \"../spec_helper\"\n"
  },
  {
    "path": "spec/adapter/sqlite_spec.cr",
    "content": "require \"../spec_helper\"\n"
  },
  {
    "path": "spec/granite/associations/belongs_to_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"belongs_to\" do\n  it \"provides a getter for the foreign entity\" do\n    teacher = Teacher.new\n    teacher.name = \"Test teacher\"\n    teacher.save\n\n    klass = Klass.new\n    klass.name = \"Test klass\"\n    klass.teacher_id = teacher.id\n    klass.save\n\n    klass.teacher.id.should eq teacher.id\n  end\n\n  it \"provides a setter for the foreign entity\" do\n    teacher = Teacher.new\n    teacher.name = \"Test teacher\"\n    teacher.save\n\n    klass = Klass.new\n    klass.name = \"Test klass\"\n    klass.teacher = teacher\n    klass.save\n\n    klass.teacher_id.should eq teacher.id\n  end\n\n  it \"supports custom types for the join\" do\n    book = Book.new\n    book.name = \"Screw driver\"\n    book.save\n\n    review = BookReview.new\n    review.book = book\n    review.body = \"Best book ever!\"\n    review.save\n\n    review.book.name.should eq \"Screw driver\"\n  end\n\n  it \"supports custom method name\" do\n    author = Person.new\n    author.name = \"John Titor\"\n    author.save\n\n    book = Book.new\n    book.name = \"How to Time Traveling\"\n    book.author = author\n    book.save\n\n    book.author.name.should eq \"John Titor\"\n  end\n\n  it \"supports both custom method name and custom types for the join\" do\n    publisher = Company.new\n    publisher.name = \"Amber Framework\"\n    publisher.save\n\n    book = Book.new\n    book.name = \"Introduction to Granite\"\n    book.publisher = publisher\n    book.save\n\n    book.publisher.name.should eq \"Amber Framework\"\n  end\n\n  it \"supports json_options\" do\n    publisher = Company.new\n    publisher.name = \"Amber Framework\"\n    publisher.save\n\n    book = Book.new\n    book.name = \"Introduction to Granite\"\n    book.publisher = publisher\n    book.save\n    book.to_json.should eq %({\"id\":#{book.id},\"name\":\"Introduction to Granite\"})\n  end\n\n  it \"supports yaml_options\" do\n    publisher = Company.new\n    publisher.name = \"Amber Framework\"\n    publisher.save\n\n    book = Book.new\n    book.name = \"Introduction to Granite\"\n    book.publisher = publisher\n    book.save\n    book.to_yaml.should eq %(---\\nid: #{book.id}\\nname: Introduction to Granite\\n)\n  end\n\n  it \"provides a method to retrieve parent object that will raise if record is not found\" do\n    book = Book.new\n    book.name = \"Introduction to Granite\"\n\n    expect_raises Granite::Querying::NotFound, \"No Company found where id is NULL\" { book.publisher! }\n  end\n\n  it \"provides the ability to use a custom primary key\" do\n    courier = Courier.new\n    courier.courier_id = 139_132_751\n    courier.issuer_id = 999\n\n    service = CourierService.new\n    service.owner_id = 123_321\n    service.name = \"My Service\"\n    service.save\n\n    courier.service = service\n    courier.save\n\n    courier.service!.owner_id.should eq 123_321\n  end\n\n  it \"allows a belongs_to association to be a primary key\" do\n    chat = Chat.new\n    chat.name = \"My Awesome Chat\"\n    chat.save\n\n    settings = ChatSettings.new\n    settings.chat = chat\n    settings.save\n\n    settings.chat_id!.should eq chat.id\n  end\n\n  it \"provides the ability to define a converter for the foreign key\" do\n    uuid_model = UUIDModel.new\n    uuid_model.save\n\n    uuid_relation = UUIDRelation.new\n    uuid_relation.uuid_model = uuid_model\n    uuid_relation.save\n\n    uuid_relation.uuid_model_id.should eq uuid_model.uuid\n  end\nend\n"
  },
  {
    "path": "spec/granite/associations/has_many_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"has_many\" do\n  it \"provides a method to retrieve associated objects\" do\n    teacher = Teacher.new\n    teacher.name = \"test teacher\"\n    teacher.save\n\n    class1 = Klass.new\n    class1.name = \"Test class 1\"\n    class1.teacher = teacher\n    class1.save\n\n    class2 = Klass.new\n    class2.name = \"Test class 2\"\n    class2.teacher = teacher\n    class2.save\n\n    class3 = Klass.new\n    class3.name = \"Test class 3\"\n    class3.save\n\n    teacher.klasses.size.should eq 2\n  end\n\n  context \"querying association\" do\n    it \"#all\" do\n      teacher = Teacher.new\n      teacher.name = \"test teacher\"\n      teacher.save\n\n      klass1 = Klass.new\n      klass1.name = \"Test class X\"\n      klass1.teacher = teacher\n      klass1.save\n\n      klass2 = Klass.new\n      klass2.name = \"Test class X\"\n      klass2.teacher = teacher\n      klass2.save\n\n      klass3 = Klass.new\n      klass3.name = \"Test class with different name\"\n      klass3.teacher = teacher\n      klass3.save\n\n      klasses = teacher.klasses.all(\"AND klasses.name = ? ORDER BY klasses.id DESC\", [\"Test class X\"])\n      klasses.map(&.id).should eq [klass2.id, klass1.id]\n    end\n\n    it \"#find_by\" do\n      teacher = Teacher.new\n      teacher.name = \"test teacher\"\n      teacher.save\n\n      klass1 = Klass.new\n      klass1.name = \"Test class X\"\n      klass1.teacher = teacher\n      klass1.save\n\n      klass2 = Klass.new\n      klass2.name = \"Test class X\"\n      klass2.teacher = teacher\n      klass2.save\n\n      klass3 = Klass.new\n      klass3.name = \"Test class with different name\"\n      klass3.teacher = teacher\n      klass3.save\n\n      klass = teacher.klasses.find_by(name: \"Test class with different name\")\n      if klass\n        klass.id.should eq klass3.id\n        klass.name.should eq \"Test class with different name\"\n      else\n        klass.should_not be_nil\n      end\n    end\n\n    it \"#find_by!\" do\n      teacher = Teacher.new\n      teacher.name = \"test teacher\"\n      teacher.save\n\n      klass1 = Klass.new\n      klass1.name = \"Test class X\"\n      klass1.teacher = teacher\n      klass1.save\n\n      klass2 = Klass.new\n      klass2.name = \"Test class X\"\n      klass2.teacher = teacher\n      klass2.save\n\n      klass3 = Klass.new\n      klass3.name = \"Test class with different name\"\n      klass3.teacher = teacher\n      klass3.save\n\n      klass = teacher.klasses.find_by!(name: \"Test class with different name\")\n      klass.id.should eq klass3.id\n      klass.name.should eq \"Test class with different name\"\n\n      expect_raises(\n        Granite::Querying::NotFound,\n        \"No #{Klass.name} found where name = not_found\"\n      ) do\n        klass = teacher.klasses.find_by!(name: \"not_found\")\n      end\n    end\n\n    it \"#find\" do\n      teacher = Teacher.new\n      teacher.name = \"test teacher\"\n      teacher.save\n\n      klass1 = Klass.new\n      klass1.name = \"Test class X\"\n      klass1.teacher = teacher\n      klass1.save\n\n      klass2 = Klass.new\n      klass2.name = \"Test class X\"\n      klass2.teacher = teacher\n      klass2.save\n\n      klass3 = Klass.new\n      klass3.name = \"Test class with different name\"\n      klass3.teacher = teacher\n      klass3.save\n\n      klass = teacher.klasses.find(klass1.id)\n      if klass\n        klass.id.should eq klass1.id\n        klass.name.should eq \"Test class X\"\n      else\n        klass.should_not be_nil\n      end\n    end\n\n    it \"#find!\" do\n      teacher = Teacher.new\n      teacher.name = \"test teacher\"\n      teacher.save\n\n      klass1 = Klass.new\n      klass1.name = \"Test class X\"\n      klass1.teacher = teacher\n      klass1.save\n\n      klass2 = Klass.new\n      klass2.name = \"Test class X\"\n      klass2.teacher = teacher\n      klass2.save\n\n      klass3 = Klass.new\n      klass3.name = \"Test class with different name\"\n      klass3.teacher = teacher\n      klass3.save\n\n      klass = teacher.klasses.find!(klass1.id)\n      klass.id.should eq klass1.id\n      klass.name.should eq \"Test class X\"\n\n      id = klass3.id.as(Int64) + 42\n\n      expect_raises(\n        Granite::Querying::NotFound,\n        \"No #{Klass.name} found where id = #{id}\"\n      ) do\n        teacher.klasses.find!(id)\n      end\n    end\n\n    it \"should respect the current primary key\" do\n      courier1 = Courier.new\n      courier1.courier_id = 1\n      courier1.issuer_id = 1\n      courier1.service_id = 1\n      courier1.save\n\n      courier2 = Courier.new\n      courier2.courier_id = 2\n      courier2.issuer_id = 2\n      courier2.service_id = 1\n      courier2.save\n\n      courier3 = Courier.new\n      courier3.courier_id = 3\n      courier3.issuer_id = 3\n      courier3.service_id = 1\n      courier3.save\n\n      service = CourierService.new\n      service.name = \"My service\"\n      service.owner_id = 1\n\n      couriers = service.couriers.to_a\n\n      couriers.size.should eq 3\n      couriers[0].courier_id.should eq courier1.courier_id\n      couriers[0].issuer_id.should eq courier1.issuer_id\n\n      couriers[1].courier_id.should eq courier2.courier_id\n      couriers[1].issuer_id.should eq courier2.issuer_id\n\n      couriers[2].courier_id.should eq courier3.courier_id\n      couriers[2].issuer_id.should eq courier3.issuer_id\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/associations/has_many_through_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"has_many, through:\" do\n  it \"provides a method to retrieve associated objects through another table\" do\n    student = Student.new\n    student.name = \"test student\"\n    student.save\n\n    unrelated_student = Student.new\n    unrelated_student.name = \"other student\"\n    unrelated_student.save\n\n    klass1 = Klass.new\n    klass1.name = \"Test class\"\n    klass1.save\n\n    klass2 = Klass.new\n    klass2.name = \"Test class\"\n    klass2.save\n\n    klass3 = Klass.new\n    klass3.name = \"Test class\"\n    klass3.save\n\n    enrollment1 = Enrollment.new\n    enrollment1.student = student\n    enrollment1.klass = klass1\n    enrollment1.save\n\n    enrollment2 = Enrollment.new\n    enrollment2.student = student\n    enrollment2.klass = klass2\n    enrollment2.save\n\n    enrollment3 = Enrollment.new\n    enrollment3.klass = klass2\n    enrollment3.student = unrelated_student\n    enrollment3.save\n\n    student.klasses.compact_map(&.id).sort!.should eq [klass1.id, klass2.id].compact.sort!\n\n    klass2.students.compact_map(&.id).sort!.should eq [student.id, unrelated_student.id].compact.sort!\n  end\n\n  context \"querying association\" do\n    it \"#all\" do\n      student = Student.create(name: \"test student\")\n\n      klass1 = Klass.create(name: \"Test class X\")\n      klass2 = Klass.create(name: \"Test class X\")\n      klass3 = Klass.create(name: \"Test class with different name\")\n\n      enrollment1 = Enrollment.new\n      enrollment1.student = student\n      enrollment1.klass = klass1\n      enrollment1.save\n\n      enrollment2 = Enrollment.new\n      enrollment2.student = student\n      enrollment2.klass = klass2\n      enrollment2.save\n\n      enrollment3 = Enrollment.new\n      enrollment3.klass = klass3\n      enrollment3.student = student\n      enrollment3.save\n\n      klasses = student.klasses.all(\"AND klasses.name = ? ORDER BY klasses.id DESC\", [\"Test class X\"])\n      klasses.map(&.id).should eq [klass2.id, klass1.id]\n    end\n\n    it \"#find_by\" do\n      student = Student.create(name: \"test student\")\n\n      klass1 = Klass.create(name: \"Test class X\")\n      klass2 = Klass.create(name: \"Test class X\")\n      klass3 = Klass.create(name: \"Test class with different name\")\n\n      enrollment1 = Enrollment.new\n      enrollment1.student = student\n      enrollment1.klass = klass1\n      enrollment1.save\n\n      enrollment2 = Enrollment.new\n      enrollment2.student = student\n      enrollment2.klass = klass2\n      enrollment2.save\n\n      enrollment3 = Enrollment.new\n      enrollment3.klass = klass3\n      enrollment3.student = student\n      enrollment3.save\n\n      klass = student.klasses.find_by(name: \"Test class with different name\")\n      if klass\n        klass.id.should eq klass3.id\n        klass.name.should eq \"Test class with different name\"\n      else\n        klass.should_not be_nil\n      end\n    end\n\n    it \"#find_by!\" do\n      student = Student.create(name: \"test student\")\n\n      klass1 = Klass.create(name: \"Test class X\")\n      klass2 = Klass.create(name: \"Test class X\")\n      klass3 = Klass.create(name: \"Test class with different name\")\n\n      enrollment1 = Enrollment.new\n      enrollment1.student = student\n      enrollment1.klass = klass1\n      enrollment1.save\n\n      enrollment2 = Enrollment.new\n      enrollment2.student = student\n      enrollment2.klass = klass2\n      enrollment2.save\n\n      enrollment3 = Enrollment.new\n      enrollment3.klass = klass3\n      enrollment3.student = student\n      enrollment3.save\n\n      klass = student.klasses.find_by!(name: \"Test class with different name\")\n      klass.id.should eq klass3.id\n      klass.name.should eq \"Test class with different name\"\n\n      expect_raises(\n        Granite::Querying::NotFound,\n        \"No #{Klass.name} found where name = not_found\"\n      ) do\n        klass = student.klasses.find_by!(name: \"not_found\")\n      end\n    end\n\n    it \"#find\" do\n      student = Student.create(name: \"test student\")\n\n      klass1 = Klass.create(name: \"Test class X\")\n      klass2 = Klass.create(name: \"Test class X\")\n      klass3 = Klass.create(name: \"Test class with different name\")\n\n      enrollment1 = Enrollment.new\n      enrollment1.student = student\n      enrollment1.klass = klass1\n      enrollment1.save\n\n      enrollment2 = Enrollment.new\n      enrollment2.student = student\n      enrollment2.klass = klass2\n      enrollment2.save\n\n      enrollment3 = Enrollment.new\n      enrollment3.klass = klass3\n      enrollment3.student = student\n      enrollment3.save\n\n      klass = student.klasses.find(klass1.id)\n      if klass\n        klass.id.should eq klass1.id\n        klass.name.should eq \"Test class X\"\n      else\n        klass.should_not be_nil\n      end\n    end\n\n    it \"#find!\" do\n      student = Student.create(name: \"test student\")\n\n      klass1 = Klass.create(name: \"Test class X\")\n      klass2 = Klass.create(name: \"Test class X\")\n      klass3 = Klass.create(name: \"Test class with different name\")\n\n      enrollment1 = Enrollment.new\n      enrollment1.student = student\n      enrollment1.klass = klass1\n      enrollment1.save\n\n      enrollment2 = Enrollment.new\n      enrollment2.student = student\n      enrollment2.klass = klass2\n      enrollment2.save\n\n      enrollment3 = Enrollment.new\n      enrollment3.klass = klass3\n      enrollment3.student = student\n      enrollment3.save\n\n      klass = student.klasses.find!(klass1.id)\n      klass.id.should eq klass1.id\n      klass.name.should eq \"Test class X\"\n\n      id = klass3.id.as(Int64) + 42\n\n      expect_raises(\n        Granite::Querying::NotFound,\n        \"No #{Klass.name} found where id = #{id}\"\n      ) do\n        student.klasses.find!(id)\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/associations/has_one_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"has_one\" do\n  before_each do\n    User.clear\n    Profile.clear\n    Courier.clear\n    Character.clear\n  end\n\n  it \"provides a setter to set childrens's foriegn_key from parent\" do\n    profile = Profile.new\n    profile.name = \"Test Profile\"\n    profile.save\n\n    user = User.new\n    user.email = \"test@domain.com\"\n    user.save\n\n    user.profile = profile\n    profile.user_id.should eq profile.id\n  end\n\n  it \"provides a method to retrieve associated objects\" do\n    profile = Profile.new\n    profile.name = \"Test Profile\"\n    profile.save\n\n    user = User.new\n    user.email = \"test@domain.com\"\n    user.save\n\n    # profile's foriegn_key is now set, so calling save again\n    user.profile = profile\n    profile.save\n\n    retrieved_profile = user.profile!\n    retrieved_profile.id.should eq profile.id\n  end\n\n  it \"provides a method to retrieve associated object that will raise if record is not found\" do\n    user = User.new\n    user.email = \"test@domain.com\"\n    user.save!\n    expect_raises Granite::Querying::NotFound, \"No Profile found where user_id = #{user.id}\" { user.profile! }\n  end\n\n  it \"provides the ability to use a custom primary key\" do\n    courier = Courier.new\n    courier.courier_id = 139_132_750\n    courier.issuer_id = 999\n\n    character = Character.new\n    character.character_id = 999\n    character.name = \"Mr Jones\"\n    character.save\n\n    courier.issuer = character\n    courier.save\n\n    courier.issuer!.character_id.should eq 999\n  end\nend\n"
  },
  {
    "path": "spec/granite/callbacks/abort_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#abort!\" do\n  before_each do\n    CallbackWithAbort.clear\n  end\n\n  context \"when create\" do\n    it \"doesn't run other callbacks if abort at before_save\" do\n      cwa = CallbackWithAbort.new(abort_at: \"before_save\", do_abort: true)\n      cwa.save\n\n      cwa.errors.map(&.to_s).should eq([\"Aborted at before_save.\"])\n      cwa.history.to_s.strip.should eq(\"\")\n      CallbackWithAbort.find(\"before_save\").should be_nil\n    end\n\n    it \"only runs before_save if abort at before_create\" do\n      cwa = CallbackWithAbort.new(abort_at: \"before_create\", do_abort: true)\n      cwa.save\n\n      cwa.errors.map(&.to_s).should eq([\"Aborted at before_create.\"])\n      cwa.history.to_s.strip.should eq <<-RUNS\n        before_save\n        RUNS\n      CallbackWithAbort.find(\"before_create\").should be_nil\n    end\n\n    it \"runs before_save, before_create and save successfully if abort at after_create\" do\n      cwa = CallbackWithAbort.new(abort_at: \"after_create\", do_abort: true)\n      cwa.save\n\n      cwa.errors.map(&.to_s).should eq([\"Aborted at after_create.\"])\n      cwa.history.to_s.strip.should eq <<-RUNS\n        before_save\n        before_create\n        RUNS\n      CallbackWithAbort.find(\"after_create\").should be_a(CallbackWithAbort)\n    end\n\n    it \"runs before_save, before_create, after_create and save successfully if abort at after_save\" do\n      cwa = CallbackWithAbort.new(abort_at: \"after_save\", do_abort: true)\n      cwa.save\n\n      cwa.errors.map(&.to_s).should eq([\"Aborted at after_save.\"])\n      cwa.history.to_s.strip.should eq <<-RUNS\n        before_save\n        before_create\n        after_create\n        RUNS\n      CallbackWithAbort.find(\"after_save\").should be_a(CallbackWithAbort)\n    end\n  end\n\n  context \"when update\" do\n    it \"doesn't run other callbacks if abort at before_save\" do\n      CallbackWithAbort.new(abort_at: \"before_save\", do_abort: false).save\n      cwa = CallbackWithAbort.find!(\"before_save\")\n      cwa.do_abort = true\n      cwa.save\n\n      cwa.errors.map(&.to_s).should eq([\"Aborted at before_save.\"])\n      cwa.history.to_s.strip.should eq(\"\")\n      CallbackWithAbort.find!(\"before_save\").do_abort.should be_false\n    end\n\n    it \"only runs before_save if abort at before_update\" do\n      CallbackWithAbort.new(abort_at: \"before_update\", do_abort: false).save\n      cwa = CallbackWithAbort.find!(\"before_update\")\n      cwa.do_abort = true\n      cwa.save\n\n      cwa.errors.map(&.to_s).should eq([\"Aborted at before_update.\"])\n      cwa.history.to_s.strip.should eq <<-RUNS\n        before_save\n        RUNS\n      CallbackWithAbort.find!(\"before_update\").do_abort.should be_false\n    end\n\n    it \"runs before_save, before_update and save successfully if abort at after_update\" do\n      CallbackWithAbort.new(abort_at: \"after_update\", do_abort: false).save\n      cwa = CallbackWithAbort.find!(\"after_update\")\n      cwa.do_abort = true\n      cwa.save\n\n      cwa.errors.map(&.to_s).should eq([\"Aborted at after_update.\"])\n      cwa.history.to_s.strip.should eq <<-RUNS\n        before_save\n        before_update\n        RUNS\n      CallbackWithAbort.find!(\"after_update\").do_abort.should be_true\n    end\n\n    it \"runs before_save, before_update, after_update and save successfully if abort at after_save\" do\n      CallbackWithAbort.new(abort_at: \"after_save\", do_abort: false).save\n      cwa = CallbackWithAbort.find!(\"after_save\")\n      cwa.do_abort = true\n      cwa.save\n\n      cwa.errors.map(&.to_s).should eq([\"Aborted at after_save.\"])\n      cwa.history.to_s.strip.should eq <<-RUNS\n        before_save\n        before_update\n        after_update\n        RUNS\n      CallbackWithAbort.find!(\"after_save\").do_abort.should be_true\n    end\n  end\n\n  context \"when destroy\" do\n    it \"doesn't run other callbacks if abort at before_destroy\" do\n      CallbackWithAbort.new(abort_at: \"before_destroy\", do_abort: true).save\n      cwa = CallbackWithAbort.find!(\"before_destroy\")\n      cwa.destroy\n\n      cwa.errors.map(&.to_s).should eq([\"Aborted at before_destroy.\"])\n      cwa.history.to_s.strip.should eq(\"\")\n      CallbackWithAbort.find(\"before_destroy\").should be_a(CallbackWithAbort)\n    end\n\n    it \"runs before_destroy and destroy successfully if abort at after_destory\" do\n      CallbackWithAbort.new(abort_at: \"after_destroy\", do_abort: true).save\n      cwa = CallbackWithAbort.find!(\"after_destroy\")\n      cwa.destroy\n\n      cwa.errors.map(&.to_s).should eq([\"Aborted at after_destroy.\"])\n      cwa.history.to_s.strip.should eq <<-RUNS\n        before_destroy\n        RUNS\n      CallbackWithAbort.find(\"after_destroy\").should be_nil\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/callbacks/callbacks_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"(callback feature)\" do\n  describe \"#save (new record)\" do\n    it \"runs before_save, before_create, after_create, after_save\" do\n      callback = Callback.new(name: \"foo\")\n      callback.save\n\n      callback.history.to_s.strip.should eq <<-EOF\n        before_save\n        before_create\n        after_create\n        after_save\n        EOF\n    end\n  end\n\n  describe \"#save\" do\n    it \"runs before_save, before_update, after_update, after_save\" do\n      Callback.new(name: \"foo\").save\n      callback = Callback.first!\n      callback.save\n\n      callback.history.to_s.strip.should eq <<-EOF\n        before_save\n        before_update\n        after_update\n        after_save\n        EOF\n    end\n  end\n\n  describe \"#destroy\" do\n    it \"runs before_destroy, after_destroy\" do\n      Callback.new(name: \"foo\").save\n      callback = Callback.first!\n      callback.destroy\n\n      callback.history.to_s.strip.should eq <<-EOF\n        before_destroy\n        after_destroy\n        EOF\n    end\n  end\n\n  describe \"an exception thrown in a hook\" do\n    it \"should not get swallowed\" do\n      callback = Callback.new(name: \"foo\")\n      # close IO in order to raise IO::Error in callback blocks\n      callback.history.close\n\n      expect_raises(IO::Error, \"Closed stream\") do\n        callback.save\n      end\n    end\n  end\n\n  describe \"manually triggered\" do\n    context \"on a single model\" do\n      it \"should successfully trigger the callback\" do\n        item = Item.new(item_name: \"item1\")\n        item.item_id.should be_nil\n\n        item.before_create\n\n        item.item_id.should be_a(String)\n      end\n    end\n\n    context \"on an array of models\" do\n      it \"should successfully trigger the callback\" do\n        items = [] of Item\n        items << Item.new(item_name: \"item1\")\n        items << Item.new(item_name: \"item2\")\n        items << Item.new(item_name: \"item3\")\n        items << Item.new(item_name: \"item4\")\n\n        items.all? { |item| item.item_id.nil? }.should be_true\n\n        items.each(&.before_create)\n\n        items.all? { |item| item.item_id.is_a?(String) }.should be_true\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/columns/primary_key_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#new\" do\n  it \"works when the primary is defined as `auto: true`\" do\n    Parent.new\n  end\n\n  it \"works when the primary is defined as `auto: false`\" do\n    Kvs.new\n  end\nend\n\ndescribe \"#new(primary_key: value)\" do\n  it \"ignores the value in default\" do\n    Parent.new(id: 1_i64).id.should eq(nil)\n  end\n\n  it \"sets the value when the primary is defined as `auto: false`\" do\n    Kvs.new(k: \"foo\").k.should eq(\"foo\")\n    Kvs.new(k: \"foo\", v: \"v\").k.should eq(\"foo\")\n  end\nend\n"
  },
  {
    "path": "spec/granite/columns/read_attribute_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"read_attribute\" do\n  # Only PG supports array types\n  {% if env(\"CURRENT_ADAPTER\") == \"pg\" %}\n    it \"able to read arrays\" do\n      ArrayModel.new.read_attribute(\"i32_array\").should be_nil\n    end\n  {% end %}\nend\n"
  },
  {
    "path": "spec/granite/columns/timestamps_spec.cr",
    "content": "require \"../../spec_helper\"\n\n# Can run this spec for sqlite after https://www.sqlite.org/draft/releaselog/3_24_0.html is released.\n{% if [\"pg\", \"mysql\"].includes? env(\"CURRENT_ADAPTER\") %}\n  describe \"timestamps\" do\n    it \"should uses UTC for created_at by default\" do\n      parent = Parent.new(name: \"parent\").tap(&.save)\n      found_parent = Parent.find!(parent.id)\n\n      original_timestamp = parent.created_at!\n      read_timestamp = found_parent.created_at!\n\n      original_timestamp.location.should eq Time::Location::UTC\n      read_timestamp.location.should eq Time::Location::UTC\n    end\n\n    it \"should uses UTC for updated_at by default\" do\n      parent = Parent.new(name: \"parent\").tap(&.save)\n      found_parent = Parent.find!(parent.id)\n\n      original_timestamp = parent.updated_at!\n      read_timestamp = found_parent.updated_at!\n\n      original_timestamp.location.should eq Time::Location::UTC\n      read_timestamp.location.should eq Time::Location::UTC\n    end\n\n    it \"should uses timezone for created_at\" do\n      Granite.settings.default_timezone = \"Asia/Shanghai\"\n\n      parent = Parent.new(name: \"parent\").tap(&.save)\n      found_parent = Parent.find!(parent.id)\n\n      original_timestamp = parent.created_at!\n      read_timestamp = found_parent.created_at!\n\n      original_timestamp.location.should eq Time::Location.load(\"Asia/Shanghai\")\n      read_timestamp.location.should eq Time::Location.load(\"Asia/Shanghai\")\n    end\n\n    it \"should uses timezone for updated_at\" do\n      Granite.settings.default_timezone = \"Asia/Shanghai\"\n\n      parent = Parent.new(name: \"parent\").tap(&.save)\n      found_parent = Parent.find!(parent.id)\n\n      original_timestamp = parent.updated_at!\n      read_timestamp = found_parent.updated_at!\n\n      original_timestamp.location.should eq Time::Location.load(\"Asia/Shanghai\")\n      read_timestamp.location.should eq Time::Location.load(\"Asia/Shanghai\")\n    end\n\n    it \"truncates the subsecond parts of created_at\" do\n      parent = Parent.new(name: \"parent\").tap(&.save)\n      found_parent = Parent.find!(parent.id)\n\n      original_timestamp = parent.created_at!\n      read_timestamp = found_parent.created_at!\n\n      original_timestamp.to_unix.should eq read_timestamp.to_unix\n    end\n\n    it \"truncates the subsecond parts of updated_at\" do\n      parent = Parent.new(name: \"parent\").tap(&.save)\n      found_parent = Parent.find!(parent.id)\n\n      original_timestamp = parent.updated_at!\n      read_timestamp = found_parent.updated_at!\n\n      original_timestamp.to_unix.should eq read_timestamp.to_unix\n    end\n\n    context \"bulk imports\" do\n      it \"timestamps are returned correctly with bulk imports\" do\n        to_import = [\n          Parent.new(name: \"ParentImport1\"),\n          Parent.new(name: \"ParentImport2\"),\n          Parent.new(name: \"ParentImport3\"),\n        ]\n\n        grandma = Parent.new(name: \"grandma\").tap(&.save)\n        found_grandma = Parent.find! grandma.id\n        Parent.import(to_import)\n\n        parents = Parent.all(\"WHERE name LIKE ?\", [\"ParentImport%\"])\n\n        parents.size.should eq 3\n\n        parents.each do |parent|\n          parent.updated_at.not_nil!.location.should eq Time::Location::UTC\n          parent.created_at.not_nil!.location.should eq Time::Location::UTC\n          found_grandma.updated_at.not_nil!.to_unix.should eq parent.updated_at.not_nil!.to_unix\n          found_grandma.created_at.not_nil!.to_unix.should eq parent.created_at.not_nil!.to_unix\n        end\n      end\n\n      it \"created_at and updated_at are correctly handled\" do\n        to_import = [\n          Parent.new(name: \"ParentOne\"),\n        ]\n\n        Parent.import(to_import)\n        import_time = Time.utc.at_beginning_of_second\n\n        parent1 = Parent.find_by!(name: \"ParentOne\")\n        parent1.name.should eq \"ParentOne\"\n        parent1.created_at!.should eq import_time\n        parent1.updated_at!.should eq import_time\n\n        to_update = Parent.all(\"WHERE name = ?\", [\"ParentOne\"])\n        to_update.each { |parent| parent.name = \"ParentOneEdited\" }\n\n        sleep 1\n\n        Parent.import(to_update, update_on_duplicate: true, columns: [\"name\"])\n        update_time = Time.utc.at_beginning_of_second\n\n        parent1_edited = Parent.find_by!(name: \"ParentOneEdited\")\n        parent1_edited.name.should eq \"ParentOneEdited\"\n        parent1_edited.created_at!.should be_close(import_time, 1.second)\n        parent1_edited.updated_at!.should be_close(update_time, 1.second)\n      end\n    end\n  end\n{% end %}\n"
  },
  {
    "path": "spec/granite/columns/uuid_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"UUID creation\" do\n  it \"correctly sets a RFC4122 V4 UUID on save\" do\n    item = UUIDModel.new\n    item.uuid.should be_nil\n    item.save\n    item.uuid.should be_a(UUID)\n    item.uuid!.version.v4?.should be_true\n    item.uuid!.variant.rfc4122?.should be_true\n  end\nend\n"
  },
  {
    "path": "spec/granite/connection_management_spec.cr",
    "content": "require \"spec\"\n\ndescribe \"Granite::Base track time since last write\" do\n  it \"should switch to reader db connection after connection_switch_wait_period after write operation\" do\n    ReplicatedChat.connection_switch_wait_period = 250\n    ReplicatedChat.new(content: \"hello world!\").save!\n    sleep 500.milliseconds\n    current_url = ReplicatedChat.adapter.url\n    reader_connection = Granite::Connections[\"#{ENV[\"CURRENT_ADAPTER\"]}_with_replica\"]\n    raise \"Reader connection cannot be nil\" if reader_connection.nil?\n    reader_url = reader_connection[:reader].url\n    current_url.should eq reader_url\n  end\nend\n"
  },
  {
    "path": "spec/granite/converters/converters_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::Converters do\n  {% if env(\"CURRENT_ADAPTER\") == \"pg\" %}\n    describe \"#save\" do\n      it \"should handle nil values\" do\n        model = ConverterModel.new\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        # Enums\n        model.smallint_enum.should be_nil\n        model.bigint_enum.should be_nil\n        model.string_enum.should be_nil\n        model.enum_enum.should be_nil\n        model.binary_enum.should be_nil\n\n        # Numeric\n        model.numeric.should be_nil\n\n        # JSON\n        model.string_json.should be_nil\n        model.string_jsonb.should be_nil\n        model.binary_json.should be_nil\n      end\n\n      it \"should handle actual values\" do\n        obj = MyType.new\n\n        model = ConverterModel.new numeric: Math::PI.round(20)\n\n        model.binary_json = model.string_jsonb = model.string_json = obj\n\n        model.smallint_enum = MyEnum::Zero\n        model.bigint_enum = MyEnum::One\n        model.string_enum = MyEnum::Two\n        model.enum_enum = MyEnum::Three\n        model.binary_enum = MyEnum::Four\n\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        # Enums\n        model.smallint_enum.should eq MyEnum::Zero\n        model.bigint_enum.should eq MyEnum::One\n        model.string_enum.should eq MyEnum::Two\n        model.enum_enum.should eq MyEnum::Three\n        model.binary_enum.should eq MyEnum::Four\n\n        # Numeric\n        model.numeric.should eq Math::PI.round(20)\n\n        # JSON\n        model.string_json.should eq obj\n        model.string_jsonb.should eq obj\n        model.binary_json.should eq obj\n      end\n    end\n\n    describe \"#read\" do\n      it \"should handle nil values\" do\n        model = ConverterModel.new\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        retrieved_model = ConverterModel.find! model.id\n\n        # Enums\n        retrieved_model.smallint_enum.should be_nil\n        retrieved_model.bigint_enum.should be_nil\n        retrieved_model.string_enum.should be_nil\n        retrieved_model.enum_enum.should be_nil\n        retrieved_model.binary_enum.should be_nil\n\n        # Numeric\n        retrieved_model.numeric.should be_nil\n\n        # JSON\n        retrieved_model.string_json.should be_nil\n        retrieved_model.string_jsonb.should be_nil\n        retrieved_model.binary_json.should be_nil\n      end\n\n      it \"should handle actual values\" do\n        obj = MyType.new\n\n        model = ConverterModel.new numeric: Math::PI.round(20)\n\n        model.binary_json = model.string_jsonb = model.string_json = obj\n\n        model.smallint_enum = MyEnum::Zero\n        model.bigint_enum = MyEnum::One\n        model.string_enum = MyEnum::Two\n        model.enum_enum = MyEnum::Three\n        model.binary_enum = MyEnum::Four\n\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        retrieved_model = ConverterModel.find! model.id\n\n        # Enum\n        retrieved_model.smallint_enum.should eq MyEnum::Zero\n        retrieved_model.bigint_enum.should eq MyEnum::One\n        retrieved_model.string_enum.should eq MyEnum::Two\n        retrieved_model.enum_enum.should eq MyEnum::Three\n        retrieved_model.binary_enum.should eq MyEnum::Four\n\n        # Numeric\n        retrieved_model.numeric.should eq Math::PI.round(20)\n\n        # JSON\n        retrieved_model.string_json.should eq obj\n        retrieved_model.string_jsonb.should eq obj\n        retrieved_model.binary_json.should eq obj\n      end\n    end\n  {% elsif env(\"CURRENT_ADAPTER\") == \"sqlite\" %}\n    describe \"#save\" do\n      it \"should handle nil values\" do\n        model = ConverterModel.new\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        # Enums\n        model.int_enum.should be_nil\n        model.string_enum.should be_nil\n        model.binary_enum.should be_nil\n\n        # JSON\n        model.string_json.should be_nil\n        model.binary_json.should be_nil\n      end\n\n      it \"should handle actual values\" do\n        obj = MyType.new\n\n        model = ConverterModel.new\n\n        model.binary_json = model.string_json = obj\n\n        model.int_enum = MyEnum::Zero\n        model.string_enum = MyEnum::Two\n        model.binary_enum = MyEnum::Four\n\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        # Enums\n        model.int_enum.should eq MyEnum::Zero\n        model.string_enum.should eq MyEnum::Two\n        model.binary_enum.should eq MyEnum::Four\n\n        # JSON\n        model.string_json.should eq obj\n        model.binary_json.should eq obj\n      end\n    end\n\n    describe \"#read\" do\n      it \"should handle nil values\" do\n        model = ConverterModel.new\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        retrieved_model = ConverterModel.find! model.id\n\n        # Enums\n        retrieved_model.int_enum.should be_nil\n        retrieved_model.string_enum.should be_nil\n        retrieved_model.binary_enum.should be_nil\n\n        # JSON\n        retrieved_model.string_json.should be_nil\n        retrieved_model.binary_json.should be_nil\n      end\n\n      it \"should handle actual values\" do\n        obj = MyType.new\n\n        model = ConverterModel.new\n\n        model.binary_json = model.string_json = obj\n\n        model.int_enum = MyEnum::Zero\n        model.string_enum = MyEnum::Two\n        model.binary_enum = MyEnum::Four\n\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        retrieved_model = ConverterModel.find! model.id\n\n        # Enums\n        retrieved_model.int_enum.should eq MyEnum::Zero\n        retrieved_model.string_enum.should eq MyEnum::Two\n        retrieved_model.binary_enum.should eq MyEnum::Four\n\n        # JSON\n        retrieved_model.string_json.should eq obj\n        retrieved_model.binary_json.should eq obj\n      end\n    end\n  {% elsif env(\"CURRENT_ADAPTER\") == \"mysql\" %}\n    describe \"#save\" do\n      it \"should handle nil values\" do\n        model = ConverterModel.new\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        # Enums\n        model.int_enum.should be_nil\n        model.string_enum.should be_nil\n        model.enum_enum.should be_nil\n        model.binary_enum.should be_nil\n\n        # JSON\n        model.string_json.should be_nil\n        model.binary_json.should be_nil\n      end\n\n      it \"should handle actual values\" do\n        obj = MyType.new\n\n        model = ConverterModel.new\n\n        model.binary_json = model.string_json = obj\n\n        model.int_enum = MyEnum::Zero\n        model.string_enum = MyEnum::Two\n        model.enum_enum = MyEnum::Three\n        model.binary_enum = MyEnum::Four\n\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        # Enums\n        model.int_enum.should eq MyEnum::Zero\n        model.string_enum.should eq MyEnum::Two\n        model.enum_enum.should eq MyEnum::Three\n        model.binary_enum.should eq MyEnum::Four\n\n        # JSON\n        model.string_json.should eq obj\n        model.binary_json.should eq obj\n      end\n    end\n\n    describe \"#read\" do\n      it \"should handle nil values\" do\n        model = ConverterModel.new\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        retrieved_model = ConverterModel.find! model.id\n\n        # Enums\n        retrieved_model.int_enum.should be_nil\n        retrieved_model.string_enum.should be_nil\n        retrieved_model.enum_enum.should be_nil\n        retrieved_model.binary_enum.should be_nil\n\n        # JSON\n        retrieved_model.string_json.should be_nil\n        retrieved_model.binary_json.should be_nil\n      end\n\n      it \"should handle actual values\" do\n        obj = MyType.new\n\n        model = ConverterModel.new\n\n        model.binary_json = model.string_json = obj\n\n        model.int_enum = MyEnum::Zero\n        model.string_enum = MyEnum::Two\n        model.enum_enum = MyEnum::Three\n        model.binary_enum = MyEnum::Four\n\n        model.save.should be_true\n        model.id.should be_a Int64\n\n        retrieved_model = ConverterModel.find! model.id\n\n        # Enums\n        retrieved_model.int_enum.should eq MyEnum::Zero\n        retrieved_model.string_enum.should eq MyEnum::Two\n        retrieved_model.enum_enum.should eq MyEnum::Three\n        retrieved_model.binary_enum.should eq MyEnum::Four\n\n        # JSON\n        retrieved_model.string_json.should eq obj\n        retrieved_model.binary_json.should eq obj\n      end\n    end\n  {% end %}\nend\n"
  },
  {
    "path": "spec/granite/converters/enum_spec.cr",
    "content": "require \"../../spec_helper\"\n\nenum TestEnum\n  Zero\n  One\n  Two\n  Three = 17\nend\n\ndescribe Granite::Converters::Enum do\n  describe Number do\n    describe \".to_db\" do\n      it \"should convert a Test enum into a Number\" do\n        Granite::Converters::Enum(TestEnum, Int8).to_db(TestEnum::One).should eq 1_i64\n        Granite::Converters::Enum(TestEnum, Float64).to_db(TestEnum::Two).should eq 2_i64\n        Granite::Converters::Enum(TestEnum, Int32).to_db(TestEnum::Three).should eq 17_i64\n      end\n    end\n\n    describe \".from_rs\" do\n      it \"should convert the RS value into a Test enum\" do\n        rs = FieldEmitter.new.tap do |e|\n          e._set_values([0])\n        end\n\n        Granite::Converters::Enum(TestEnum, Int32).from_rs(rs).should eq TestEnum::Zero\n      end\n\n      it \"should convert the RS value into a Test enum\" do\n        rs = FieldEmitter.new.tap do |e|\n          e._set_values([1_i16])\n        end\n\n        Granite::Converters::Enum(TestEnum, Int16).from_rs(rs).should eq TestEnum::One\n      end\n\n      it \"should convert the RS value into a Test enum\" do\n        rs = FieldEmitter.new.tap do |e|\n          e._set_values([17.0])\n        end\n\n        Granite::Converters::Enum(TestEnum, Float64).from_rs(rs).should eq TestEnum::Three\n      end\n    end\n  end\n\n  describe String do\n    describe \".to_db\" do\n      it \"should convert a Test enum into a string\" do\n        Granite::Converters::Enum(TestEnum, String).to_db(TestEnum::Two).should eq \"Two\"\n      end\n    end\n\n    describe \".from_rs\" do\n      it \"should convert the RS value into a Test enum\" do\n        rs = FieldEmitter.new.tap do |e|\n          e._set_values([\"Three\"])\n        end\n\n        Granite::Converters::Enum(TestEnum, String).from_rs(rs).should eq TestEnum::Three\n      end\n    end\n  end\n\n  describe Bytes do\n    describe \".to_db\" do\n      it \"should convert a Test enum into a string\" do\n        Granite::Converters::Enum(TestEnum, Bytes).to_db(TestEnum::Two).should eq \"Two\"\n      end\n    end\n\n    describe \".from_rs\" do\n      it \"should convert an Int32 value into a Test enum\" do\n        rs = FieldEmitter.new.tap do |e|\n          e._set_values([Bytes[90, 101, 114, 111]])\n        end\n\n        Granite::Converters::Enum(TestEnum, Bytes).from_rs(rs).should eq TestEnum::Zero\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/converters/json_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::Converters::Json do\n  describe String do\n    describe \".to_db\" do\n      it \"should convert an Object into a String\" do\n        Granite::Converters::Json(MyType, String).to_db(MyType.new).should eq MyType.new.to_json\n      end\n    end\n\n    describe \".from_rs\" do\n      it \"should convert the RS value into a MyType\" do\n        rs = FieldEmitter.new.tap do |e|\n          e._set_values([MyType.new.to_json])\n        end\n\n        Granite::Converters::Json(MyType, String).from_rs(rs).should eq MyType.new\n      end\n    end\n  end\n\n  describe String do\n    describe \".to_db\" do\n      it \"should convert an Object into a String\" do\n        Granite::Converters::Json(MyType, JSON::Any).to_db(MyType.new).should eq MyType.new.to_json\n      end\n    end\n\n    describe \".from_rs\" do\n      it \"should convert the RS value into a MyType\" do\n        rs = FieldEmitter.new.tap do |e|\n          e._set_values([JSON.parse(MyType.new.to_json)])\n        end\n\n        Granite::Converters::Json(MyType, JSON::Any).from_rs(rs).should eq MyType.new\n      end\n    end\n  end\n\n  describe Bytes do\n    describe \".to_db\" do\n      it \"should convert an Object into Bytes\" do\n        Granite::Converters::Json(MyType, Bytes).to_db(MyType.new).should eq MyType.new.to_json.to_slice\n      end\n    end\n\n    describe \".from_rs\" do\n      it \"should convert the RS value into a MyType\" do\n        rs = FieldEmitter.new.tap do |e|\n          e._set_values([\n            Bytes[123,\n              34,\n              110,\n              97,\n              109,\n              101,\n              34,\n              58,\n              34,\n              74,\n              105,\n              109,\n              34,\n              44,\n              34,\n              97,\n              103,\n              101,\n              34,\n              58,\n              49,\n              50,\n              125],\n          ])\n        end\n\n        Granite::Converters::Json(MyType, Bytes).from_rs(rs).should eq MyType.new\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/converters/pg_numeric_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::Converters::PgNumeric do\n  describe \".to_db\" do\n    it \"should convert a Float enum into a Float\" do\n      Granite::Converters::PgNumeric.to_db(3.14).should eq 3.14\n    end\n  end\n\n  describe \".from_rs\" do\n    it \"should convert the RS value into a Float64\" do\n      rs = FieldEmitter.new.tap do |e|\n        e._set_values([PG::Numeric.new(2_i16, 0_i16, 0_i16, 1_i16, [1_i16, 3000_i16])])\n      end\n\n      Granite::Converters::PgNumeric.from_rs(rs).should eq 1.3\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/error/error_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::Error do\n  it \"should convert to json\" do\n    Granite::Error.new(\"field\", \"error message\").to_json.should eq %({\"field\":\"field\",\"message\":\"error message\"})\n  end\nend\n"
  },
  {
    "path": "spec/granite/exceptions/record_invalid_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::RecordNotSaved do\n  it \"should have a message\" do\n    parent = Parent.new\n    parent.save\n\n    Granite::RecordNotSaved\n      .new(Parent.name, parent)\n      .message\n      .should eq(\"Could not process Parent: Name cannot be blank\")\n  end\n\n  it \"should have a model\" do\n    parent = Parent.new\n    parent.save\n\n    Granite::RecordNotSaved\n      .new(Parent.name, parent)\n      .model\n      .should eq(parent)\n  end\nend\n"
  },
  {
    "path": "spec/granite/exceptions/record_not_destroyed_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::RecordNotDestroyed do\n  it \"should have a message\" do\n    parent = Parent.new\n    parent.save\n\n    Granite::RecordNotDestroyed\n      .new(Parent.name, parent)\n      .message\n      .should eq(\"Could not destroy Parent: Name cannot be blank\")\n  end\n\n  it \"should have a model\" do\n    parent = Parent.new\n    parent.save\n\n    Granite::RecordNotDestroyed\n      .new(Parent.name, parent)\n      .model\n      .should eq(parent)\n  end\nend\n"
  },
  {
    "path": "spec/granite/integrators/find_or_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"find_or_create_by, find_or_initialize_by\" do\n  it \"creates on find_or_create when not found\" do\n    Parent.clear\n    Parent.find_or_create_by(name: \"name\")\n    Parent.first!.name.should eq(\"name\")\n    Parent.first!.new_record?.should eq(false)\n  end\n\n  it \"uses find on find_or_create_by when it exists\" do\n    Parent.clear\n    Parent.create(name: \"name\")\n    Parent.find_or_create_by(name: \"name\")\n    Parent.count.should eq(1)\n  end\n\n  it \"uses find on find_or_initialize_by when it exists\" do\n    Parent.clear\n    Parent.create(name: \"name\")\n    parent = Parent.find_or_initialize_by(name: \"name\")\n    parent.new_record?.should eq(false)\n  end\n\n  it \"initializes with find_or_initialize when not found\" do\n    Parent.clear\n    parent = Parent.find_or_initialize_by(name: \"gnome\")\n    parent.new_record?.should eq(true)\n  end\nend\n"
  },
  {
    "path": "spec/granite/migrator/migrator_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::Migrator do\n  describe \"#drop_sql\" do\n    it \"generates correct SQL with #{{{ env(\"CURRENT_ADAPTER\") }}} adapter\" do\n      {% if env(\"CURRENT_ADAPTER\") == \"mysql\" %}\n        Review.migrator.drop_sql.should eq \"DROP TABLE IF EXISTS `reviews`;\"\n      {% else %}\n        Review.migrator.drop_sql.should eq \"DROP TABLE IF EXISTS \\\"reviews\\\";\"\n      {% end %}\n    end\n  end\n\n  describe \"#create_sql\" do\n    it \"generates correct SQL with #{{{ env(\"CURRENT_ADAPTER\") }}} adapter\" do\n      {% if env(\"CURRENT_ADAPTER\") == \"pg\" %}\n        Review.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"reviews\"(\n          \"id\" BIGSERIAL PRIMARY KEY,\n          \"name\" TEXT\n          ,\n          \"downvotes\" INT\n          ,\n          \"upvotes\" BIGINT\n          ,\n          \"sentiment\" REAL\n          ,\n          \"interest\" DOUBLE PRECISION\n          ,\n          \"published\" BOOL\n          ,\n          \"created_at\" TIMESTAMP\n          ) ;\\n\n          SQL\n\n        Kvs.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"kvs\"(\n          \"k\" TEXT PRIMARY KEY,\n          \"v\" TEXT\n          ) ;\\n\n          SQL\n\n        UUIDModel.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"uuids\"(\n          \"uuid\" UUID PRIMARY KEY) ;\\n\n          SQL\n\n        Character.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"characters\"(\n          \"character_id\" SERIAL PRIMARY KEY,\n          \"name\" TEXT NOT NULL\n          ) ;\\n\n          SQL\n\n        # Also check Array types for pg\n        ArrayModel.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"array_model\"(\n          \"id\" SERIAL PRIMARY KEY,\n          \"str_array\" TEXT[]\n          ,\n          \"i16_array\" SMALLINT[]\n          ,\n          \"i32_array\" INT[]\n          ,\n          \"i64_array\" BIGINT[]\n          ,\n          \"f32_array\" REAL[]\n          ,\n          \"f64_array\" DOUBLE PRECISION[]\n          ,\n          \"bool_array\" BOOLEAN[]\n          ) ;\\n\n          SQL\n      {% elsif env(\"CURRENT_ADAPTER\") == \"mysql\" %}\n        Review.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE `reviews`(\n          `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n          `name` VARCHAR(255)\n          ,\n          `downvotes` INT\n          ,\n          `upvotes` BIGINT\n          ,\n          `sentiment` FLOAT\n          ,\n          `interest` DOUBLE\n          ,\n          `published` BOOL\n          ,\n          `created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP\n          ) ;\\n\n          SQL\n\n        Kvs.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE `kvs`(\n          `k` VARCHAR(255) PRIMARY KEY,\n          `v` VARCHAR(255)\n          ) ;\\n\n          SQL\n\n        Character.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE `characters`(\n          `character_id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n          `name` VARCHAR(255) NOT NULL\n          ) ;\\n\n          SQL\n\n        UUIDModel.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE `uuids`(\n          `uuid` CHAR(36) PRIMARY KEY) ;\\n\n          SQL\n      {% elsif env(\"CURRENT_ADAPTER\") == \"sqlite\" %}\n        Review.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"reviews\"(\n          \"id\" INTEGER NOT NULL PRIMARY KEY,\n          \"name\" VARCHAR(255)\n          ,\n          \"downvotes\" INTEGER\n          ,\n          \"upvotes\" INTEGER\n          ,\n          \"sentiment\" FLOAT\n          ,\n          \"interest\" REAL\n          ,\n          \"published\" BOOL\n          ,\n          \"created_at\" VARCHAR\n          ) ;\\n\n          SQL\n\n        Kvs.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"kvs\"(\n          \"k\" VARCHAR(255) PRIMARY KEY,\n          \"v\" VARCHAR(255)\n          ) ;\\n\n          SQL\n\n        Character.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"characters\"(\n          \"character_id\" INTEGER NOT NULL PRIMARY KEY,\n          \"name\" VARCHAR(255) NOT NULL\n          ) ;\\n\n          SQL\n\n        UUIDModel.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"uuids\"(\n          \"uuid\" CHAR(36) PRIMARY KEY) ;\\n\n          SQL\n      {% end %}\n    end\n\n    it \"supports a manually supplied column type\" do\n      {% if env(\"CURRENT_ADAPTER\") == \"pg\" %}\n        ManualColumnType.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"manual_column_types\"(\n          \"id\" BIGSERIAL PRIMARY KEY,\n          \"foo\" DECIMAL(12, 10)\n          ) ;\\n\n          SQL\n      {% elsif env(\"CURRENT_ADAPTER\") == \"mysql\" %}\n        ManualColumnType.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE `manual_column_types`(\n          `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,\n          `foo` DECIMAL(12, 10)\n          ) ;\\n\n          SQL\n      {% elsif env(\"CURRENT_ADAPTER\") == \"sqlite\" %}\n        ManualColumnType.migrator.create_sql.should eq <<-SQL\n          CREATE TABLE \"manual_column_types\"(\n          \"id\" INTEGER NOT NULL PRIMARY KEY,\n          \"foo\" DECIMAL(12, 10)\n          ) ;\\n\n          SQL\n      {% end %}\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/query/assemblers/mysql_spec.cr",
    "content": "require \"../spec_helper\"\n\n{% if env(\"CURRENT_ADAPTER\").id == \"mysql\" %}\n  describe Granite::Query::Assembler::Mysql(Model) do\n    context \"count\" do\n      it \"counts for where/count queries\" do\n        sql = \"select count(*) from table where name = ?\"\n        builder.where(name: \"bob\").count.raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"simple counts\" do\n        sql = \"select count(*) from table\"\n        builder.count.raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds group_by fields for where/count queries\" do\n        sql = \"select count(*) from table where name = ? group by name\"\n        builder.where(name: \"bob\").group_by(:name).count.raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"group_by\" do\n      it \"adds group_by for select query\" do\n        sql = \"select #{query_fields} from table group by name order by id desc\"\n        builder.group_by(:name).raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds multiple group_by for select query\" do\n        sql = \"select #{query_fields} from table group by name, age order by id desc\"\n        builder.group_by([:name, :age]).raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds chain of group_by for select query\" do\n        sql = \"select #{query_fields} from table group by id, name, age order by id desc\"\n        builder.group_by(:id).group_by([:name, :age]).raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"where\" do\n      it \"properly numbers fields\" do\n        sql = \"select #{query_fields} from table where name = ? and age = ? order by id desc\"\n        query = builder.where(name: \"bob\", age: \"23\")\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [\"bob\", \"23\"]\n      end\n\n      it \"property defines IN query\" do\n        sql = \"SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND status IN (?,?) ORDER BY id DESC\"\n        query = builder.where(date_completed: nil, status: [\"outstanding\", \"in_progress\"])\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [\"outstanding\", \"in_progress\"]\n      end\n\n      it \"property defines IN query with numbers\" do\n        sql = \"SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND id IN (1,2) ORDER BY id DESC\"\n        query = builder.where(date_completed: nil, id: [1, 2])\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [] of Granite::Columns::Type\n      end\n\n      it \"property defines IN query with booleans\" do\n        sql = \"SELECT #{query_fields} FROM table WHERE published IN (true,false) ORDER BY id DESC\"\n        query = builder.where(published: [true, false])\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [] of Granite::Columns::Type\n      end\n\n      it \"handles raw SQL\" do\n        sql = \"select #{query_fields} from table where name = 'bob' and age = ? and color = ? order by id desc\"\n        query = builder.where(\"name = 'bob'\").where(\"age = ?\", 23).where(\"color = ?\", \"red\")\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [23, \"red\"]\n      end\n    end\n\n    context \"order\" do\n      it \"uses default sort when no sort is provided\" do\n        builder.raw_sql.should match ignore_whitespace \"select #{query_fields} from table order by id desc\"\n      end\n\n      it \"uses specified sort when provided\" do\n        sql = \"select #{query_fields} from table order by id asc\"\n        builder.order(id: :asc).raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"offset\" do\n      it \"adds offset for select query\" do\n        sql = \"select #{query_fields} from table order by id desc offset 8\"\n        builder.offset(8).raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds offset for first query\" do\n        sql = \"select #{query_fields} from table order by id desc limit 1 offset 3\"\n        builder.offset(3).assembler.first.raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"limit\" do\n      it \"adds limit for select query\" do\n        sql = \"select #{query_fields} from table order by id desc limit 5\"\n        builder.limit(5).raw_sql.should match ignore_whitespace sql\n      end\n    end\n  end\n{% end %}\n"
  },
  {
    "path": "spec/granite/query/assemblers/pg_spec.cr",
    "content": "require \"../spec_helper\"\n\n{% if env(\"CURRENT_ADAPTER\").id == \"pg\" %}\n  describe Granite::Query::Assembler::Pg(Model) do\n    context \"count\" do\n      it \"counts for where/count queries\" do\n        sql = \"select count(*) from table where name = $1\"\n        builder.where(name: \"bob\").count.raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"simple counts\" do\n        sql = \"select count(*) from table\"\n        builder.count.raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds group_by fields for where/count queries\" do\n        sql = \"select count(*) from table where name = $1 group by name\"\n        builder.where(name: \"bob\").group_by(:name).count.raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"group_by\" do\n      it \"adds group_by for select query\" do\n        sql = \"select #{query_fields} from table group by name order by id desc\"\n        builder.group_by(:name).raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds multiple group_by for select query\" do\n        sql = \"select #{query_fields} from table group by name, age order by id desc\"\n        builder.group_by([:name, :age]).raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds chain of group_by for select query\" do\n        sql = \"select #{query_fields} from table group by id, name, age order by id desc\"\n        builder.group_by(:id).group_by([:name, :age]).raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"where\" do\n      it \"properly numbers fields\" do\n        sql = \"select #{query_fields} from table where name = $1 and age = $2 order by id desc\"\n        query = builder.where(name: \"bob\", age: \"23\")\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [\"bob\", \"23\"]\n      end\n\n      it \"property defines IN query\" do\n        sql = \"SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND status IN ($1,$2) ORDER BY id DESC\"\n        query = builder.where(date_completed: nil, status: [\"outstanding\", \"in_progress\"])\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [\"outstanding\", \"in_progress\"]\n      end\n\n      it \"property defines IN query with numbers\" do\n        sql = \"SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND id IN (1,2) ORDER BY id DESC\"\n        query = builder.where(date_completed: nil, id: [1, 2])\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [] of Granite::Columns::Type\n      end\n\n      it \"property defines IN query with booleans\" do\n        sql = \"SELECT #{query_fields} FROM table WHERE published IN (true,false) ORDER BY id DESC\"\n        query = builder.where(published: [true, false])\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [] of Granite::Columns::Type\n      end\n\n      it \"handles raw SQL\" do\n        sql = \"select #{query_fields} from table where name = 'bob' and age = $1 and color = $2 order by id desc\"\n        query = builder.where(\"name = 'bob'\").where(\"age = $\", 23).where(\"color = $\", \"red\")\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [23, \"red\"]\n      end\n    end\n\n    context \"order\" do\n      it \"uses default sort when no sort is provided\" do\n        builder.raw_sql.should match ignore_whitespace \"select #{query_fields} from table order by id desc\"\n      end\n\n      it \"uses specified sort when provided\" do\n        sql = \"select #{query_fields} from table order by id asc\"\n        builder.order(id: :asc).raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"offset\" do\n      it \"adds offset for select query\" do\n        sql = \"select #{query_fields} from table order by id desc offset 8\"\n        builder.offset(8).raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds offset for first query\" do\n        sql = \"select #{query_fields} from table order by id desc limit 1 offset 3\"\n        builder.offset(3).assembler.first.raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"limit\" do\n      it \"adds limit for select query\" do\n        sql = \"select #{query_fields} from table order by id desc limit 5\"\n        builder.limit(5).raw_sql.should match ignore_whitespace sql\n      end\n    end\n  end\n{% end %}\n"
  },
  {
    "path": "spec/granite/query/assemblers/sqlite_spec.cr",
    "content": "require \"../spec_helper\"\n\n{% if env(\"CURRENT_ADAPTER\").id == \"sqlite\" %}\n  describe Granite::Query::Assembler::Sqlite(Model) do\n    context \"count\" do\n      it \"counts for where/count queries\" do\n        sql = \"select count(*) from table where name = ?\"\n        builder.where(name: \"bob\").count.raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"simple counts\" do\n        sql = \"select count(*) from table\"\n        builder.count.raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds group_by fields for where/count queries\" do\n        sql = \"select count(*) from table where name = ? group by name\"\n        builder.where(name: \"bob\").group_by(:name).count.raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"group_by\" do\n      it \"adds group_by for select query\" do\n        sql = \"select #{query_fields} from table group by name order by id desc\"\n        builder.group_by(:name).raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds multiple group_by for select query\" do\n        sql = \"select #{query_fields} from table group by name, age order by id desc\"\n        builder.group_by([:name, :age]).raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds chain of group_by for select query\" do\n        sql = \"select #{query_fields} from table group by id, name, age order by id desc\"\n        builder.group_by(:id).group_by([:name, :age]).raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"where\" do\n      it \"properly numbers fields\" do\n        sql = \"select #{query_fields} from table where name = ? and age = ? order by id desc\"\n        query = builder.where(name: \"bob\", age: \"23\")\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [\"bob\", \"23\"]\n      end\n\n      it \"property defines IN query\" do\n        sql = \"SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND status IN (?,?) ORDER BY id DESC\"\n        query = builder.where(date_completed: nil, status: [\"outstanding\", \"in_progress\"])\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [\"outstanding\", \"in_progress\"]\n      end\n\n      it \"property defines IN query with numbers\" do\n        sql = \"SELECT #{query_fields} FROM table WHERE date_completed IS NULL AND id IN (1,2) ORDER BY id DESC\"\n        query = builder.where(date_completed: nil, id: [1, 2])\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [] of Granite::Columns::Type\n      end\n\n      it \"property defines IN query with booleans\" do\n        sql = \"SELECT #{query_fields} FROM table WHERE published IN (true,false) ORDER BY id DESC\"\n        query = builder.where(published: [true, false])\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [] of Granite::Columns::Type\n      end\n\n      it \"handles raw SQL\" do\n        sql = \"select #{query_fields} from table where name = 'bob' and age = ? and color = ? order by id desc\"\n        query = builder.where(\"name = 'bob'\").where(\"age = ?\", 23).where(\"color = ?\", \"red\")\n        query.raw_sql.should match ignore_whitespace sql\n\n        assembler = query.assembler\n        assembler.where\n        assembler.numbered_parameters.should eq [23, \"red\"]\n      end\n    end\n\n    context \"order\" do\n      it \"uses default sort when no sort is provided\" do\n        builder.raw_sql.should match ignore_whitespace \"select #{query_fields} from table order by id desc\"\n      end\n\n      it \"uses specified sort when provided\" do\n        sql = \"select #{query_fields} from table order by id asc\"\n        builder.order(id: :asc).raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"offset\" do\n      it \"adds offset for select query\" do\n        sql = \"select #{query_fields} from table order by id desc offset 8\"\n        builder.offset(8).raw_sql.should match ignore_whitespace sql\n      end\n\n      it \"adds offset for first query\" do\n        sql = \"select #{query_fields} from table order by id desc limit 1 offset 3\"\n        builder.offset(3).assembler.first.raw_sql.should match ignore_whitespace sql\n      end\n    end\n\n    context \"limit\" do\n      it \"adds limit for select query\" do\n        sql = \"select #{query_fields} from table order by id desc limit 5\"\n        builder.limit(5).raw_sql.should match ignore_whitespace sql\n      end\n    end\n  end\n{% end %}\n"
  },
  {
    "path": "spec/granite/query/builder_spec.cr",
    "content": "require \"./spec_helper\"\n\ndescribe Granite::Query::Builder(Model) do\n  it \"stores where_fields\" do\n    query = builder.where(name: \"bob\").where(age: 23)\n    expected = [{join: :and, field: \"name\", operator: :eq, value: \"bob\"}, {join: :and, field: \"age\", operator: :eq, value: 23}]\n    query.where_fields.should eq expected\n  end\n\n  it \"stores operators with where_fields\" do\n    query = builder.where(:name, :like, \"bob*\").where(:age, :gt, 23)\n    expected = [{join: :and, field: \"name\", operator: :like, value: \"bob*\"}, {join: :and, field: \"age\", operator: :gt, value: 23}]\n    query.where_fields.should eq expected\n  end\n\n  it \"stores joins with where_fields\" do\n    query = builder.where(:name, :like, \"bob*\").or(:age, :gt, 23)\n    expected = [{join: :and, field: \"name\", operator: :like, value: \"bob*\"}, {join: :or, field: \"age\", operator: :gt, value: 23}]\n    query.where_fields.should eq expected\n  end\n\n  it \"stores order fields\" do\n    query = builder.order(name: :desc).order(age: :asc)\n    expected = [\n      {field: \"name\", direction: Granite::Query::Builder::Sort::Descending},\n      {field: \"age\", direction: Granite::Query::Builder::Sort::Ascending},\n    ]\n    query.order_fields.should eq expected\n  end\n\n  it \"maps array to :in\" do\n    query = builder.where(date_completed: nil, status: [\"outstanding\", \"in_progress\"])\n    expected = [\n      {join: :and, field: \"date_completed\", operator: :eq, value: nil},\n      {join: :and, field: \"status\", operator: :in, value: [\"outstanding\", \"in_progress\"]},\n    ]\n\n    query.where_fields.should eq expected\n  end\n\n  it \"stores limit\" do\n    query = builder.limit(7)\n    query.limit.should eq 7\n  end\n\n  it \"stores offset\" do\n    query = builder.offset(17)\n    query.offset.should eq 17\n  end\n\n  context \"raw SQL builder\" do\n    placeholders = {\n      Granite::Query::Builder::DbType::Mysql  => \"?\",\n      Granite::Query::Builder::DbType::Sqlite => \"?\",\n      Granite::Query::Builder::DbType::Pg     => \"$\",\n    }\n\n    it \"chains where statements\" do\n      placeholder = placeholders[builder.db_type]\n      query = builder.where(\"name = #{placeholder}\", \"bob\").where(\"age = #{placeholder}\", 23)\n      expected = [{join: :and, stmt: \"name = #{placeholder}\", value: \"bob\"}, {join: :and, stmt: \"age = #{placeholder}\", value: 23}]\n\n      query.where_fields.should eq expected\n    end\n\n    it \"chains and statements\" do\n      placeholder = placeholders[builder.db_type]\n      query = builder.where(\"name = #{placeholder}\", \"bob\").and(\"age = #{placeholder}\", 23)\n      expected = [{join: :and, stmt: \"name = #{placeholder}\", value: \"bob\"}, {join: :and, stmt: \"age = #{placeholder}\", value: 23}]\n\n      query.where_fields.should eq expected\n    end\n\n    it \"chains or statements\" do\n      placeholder = placeholders[builder.db_type]\n      query = builder.where(\"name = #{placeholder}\", \"bob\").or(\"age = #{placeholder}\", 23)\n      expected = [{join: :and, stmt: \"name = #{placeholder}\", value: \"bob\"}, {join: :or, stmt: \"age = #{placeholder}\", value: 23}]\n\n      query.where_fields.should eq expected\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/query/executor_spec.cr",
    "content": "# default value when a Value query is run\n# delegates properly\n"
  },
  {
    "path": "spec/granite/query/spec_helper.cr",
    "content": "require \"spec\"\nrequire \"db\"\nrequire \"../../../src/granite/query/builder\"\n\nclass Model\n  def self.table_name\n    \"table\"\n  end\n\n  def self.fields\n    [\"name\", \"age\"]\n  end\n\n  def self.primary_name\n    \"id\"\n  end\nend\n\ndef query_fields\n  Model.fields.join \", \"\nend\n\ndef builder\n  {% if env(\"CURRENT_ADAPTER\").id == \"pg\" %}\n    Granite::Query::Builder(Model).new Granite::Query::Builder::DbType::Pg\n  {% elsif env(\"CURRENT_ADAPTER\").id == \"mysql\" %}\n    Granite::Query::Builder(Model).new Granite::Query::Builder::DbType::Mysql\n  {% else %}\n    Granite::Query::Builder(Model).new Granite::Query::Builder::DbType::Sqlite\n  {% end %}\nend\n\ndef ignore_whitespace(expected : String)\n  whitespace = \"\\\\s+?\"\n  compiled = expected.split(/\\s/).map { |s| Regex.escape s }.join(whitespace)\n  Regex.new \"^\\\\s*#{compiled}\\\\s*$\", Regex::Options::IGNORE_CASE ^ Regex::Options::MULTILINE\nend\n"
  },
  {
    "path": "spec/granite/querying/all_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#all\" do\n  it \"finds all the records\" do\n    Parent.clear\n    model_ids = (0...100).map do |i|\n      Parent.new(name: \"model_#{i}\").tap(&.save)\n    end.map(&.id)\n\n    all = Parent.all\n    all.size.should eq model_ids.size\n    all.compact_map(&.id).sort!.should eq model_ids.compact\n  end\n\n  # TODO Fails under MySQL\n  # it \"finds records with numbered query substition\" do\n  #   name = \"findable model\"\n  #   model = Parent.new(name: name).tap(&.save)\n  #   set = Parent.all(\"WHERE name = $1\", [name])\n  #   set.size.should eq 1\n  #   set.first.name.should eq name\n  # end\n\n  it \"finds records with question mark substition\" do\n    name = \"findable model\"\n    Parent.new(name: name).save\n    set = Parent.all(\"WHERE name = ?\", [name])\n    set.size.should eq 1\n    set.first.name.should eq name\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/count_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#count\" do\n  it \"returns 0 if no result\" do\n    Parent.clear\n    count = Parent.count\n    count.should eq 0\n  end\n\n  it \"returns a number of the all records for the model\" do\n    count = Parent.count\n    2.times do |i|\n      Parent.new(name: \"model_#{i}\").tap(&.save)\n    end\n\n    (Parent.count - count).should eq 2\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/exists_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \".exists?\" do\n  before_each do\n    Parent.clear\n  end\n\n  describe \"when there is a record with that ID\" do\n    describe \"with a numeric PK\" do\n      it \"should return true\" do\n        model = Parent.new(name: \"Some Name\")\n        model.save.should be_true\n        Parent.exists?(model.id).should be_true\n      end\n    end\n\n    describe \"with a string PK\" do\n      it \"should return true\" do\n        Kvs.new(k: \"EXISTS_ID\").save.should be_true\n        Kvs.exists?(\"EXISTS_ID\").should be_true\n      end\n    end\n\n    describe \"with a namedtuple of args\" do\n      it \"should return true\" do\n        model = Parent.new(name: \"Some Name\")\n        model.save.should be_true\n        Parent.exists?(name: \"Some Name\", id: model.id).should be_true\n      end\n    end\n\n    describe \"with a hash of args\" do\n      it \"should return true\" do\n        model = Parent.new(name: \"Some Name\")\n        model.save.should be_true\n        Parent.exists?({:name => \"Some Name\", \"id\" => model.id}).should be_true\n      end\n    end\n\n    describe \"with a nil value\" do\n      it \"should return true\" do\n        model = Student.new\n        model.save.should be_true\n        Student.exists?(name: nil, id: model.id).should be_true\n      end\n    end\n  end\n\n  describe \"when there is not a record with that ID\" do\n    describe \"with a numeric PK\" do\n      it \"should return false\" do\n        Parent.exists?(234567).should be_false\n      end\n    end\n\n    describe \"with a string PK\" do\n      it \"should return false\" do\n        Kvs.exists?(\"SOME_KEY\").should be_false\n      end\n    end\n\n    describe \"with a namedtuple of args\" do\n      it \"should return false\" do\n        model = Parent.new(name: \"Some Name\")\n        model.save.should be_true\n        Parent.exists?(name: \"Some Other Name\", id: model.id).should be_false\n      end\n    end\n\n    describe \"with a hash of args\" do\n      it \"should return false\" do\n        model = Parent.new(name: \"Some Name\")\n        model.save.should be_true\n        Parent.exists?({:name => \"Some Other Name\", \"id\" => model.id}).should be_false\n      end\n    end\n\n    describe \"with a nil value\" do\n      it \"should return false\" do\n        model = Student.new(name: \"Jim\")\n        model.save.should be_true\n        Student.exists?(name: nil, id: model.id).should be_false\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/find_by_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#find_by, #find_by!\" do\n  it \"finds an object with a string field\" do\n    Parent.clear\n    name = \"robinson\"\n\n    model = Parent.new(name: name)\n    model.save.should be_true\n\n    found = Parent.find_by(name: name)\n    if pa = found\n      pa.id.should eq model.id\n    else\n      pa.should_not be_nil\n    end\n\n    found = Parent.find_by!(name: name)\n    found.should be_a(Parent)\n  end\n\n  it \"works with multiple arguments\" do\n    Review.clear\n\n    Review.create(name: \"review1\", upvotes: 2.to_i64)\n    Review.create(name: \"review2\", upvotes: 0.to_i64)\n\n    expected = Review.create(name: \"review3\", upvotes: 10.to_i64)\n\n    r = Review.find_by(name: \"review3\", upvotes: 10)\n    if r\n      r.id.should eq expected.id\n    else\n      r.should_not be_nil\n    end\n\n    expect_raises(Granite::Querying::NotFound, /No .*Review.* found where name = review1 and upvotes = 20/) do\n      Review.find_by!(name: \"review1\", upvotes: 20)\n    end\n  end\n\n  it \"finds an object with nil value\" do\n    Student.clear\n\n    model = Student.new\n    model.save.should be_true\n\n    found = Student.find_by(name: nil)\n    if stu = found\n      stu.id.should eq model.id\n    else\n      stu.should_not be_nil\n    end\n\n    found = Student.find_by!(name: nil)\n    found.should be_a(Student)\n  end\n\n  it \"works with reserved words\" do\n    Parent.clear\n    value = \"robinson\"\n\n    model = ReservedWord.new\n    model.all = value\n    model.save.should be_true\n\n    found = ReservedWord.find_by(all: value)\n    if rw = found\n      rw.id.should eq model.id\n    else\n      rw.should_not be_nil\n    end\n\n    found = ReservedWord.find_by!(all: value)\n    found.id.should eq model.id\n  end\n\n  it \"finds an object when provided a hash\" do\n    Parent.clear\n    name = \"johnson\"\n\n    model = Parent.new(name: name)\n    model.save.should be_true\n\n    found = Parent.find_by({\"name\" => name})\n    if pa = found\n      pa.id.should eq model.id\n    else\n      pa.should_not be_nil\n    end\n\n    found = Parent.find_by!({\"name\" => name})\n    found.should be_a(Parent)\n  end\n\n  it \"returns nil or raises if no result\" do\n    Parent.clear\n    found = Parent.find_by(name: \"xxx\")\n    found.should be_nil\n\n    expect_raises(Granite::Querying::NotFound, /No .*Parent.* found where name = xxx/) do\n      Parent.find_by!(name: \"xxx\")\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/find_each_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#find_each\" do\n  it \"finds all the records\" do\n    Parent.clear\n    model_ids = (0...100).map do |i|\n      Parent.new(name: \"role_#{i}\").tap(&.save)\n    end.map(&.id)\n\n    found_roles = [] of Int64 | Nil\n    Parent.find_each do |model|\n      found_roles << model.id\n    end\n\n    found_roles.compact.sort!.should eq model_ids.compact\n  end\n\n  it \"doesnt yield when no records are found\" do\n    Parent.clear\n    Parent.find_each do\n      fail \"did yield\"\n    end\n  end\n\n  it \"can start from an offset\" do\n    Parent.clear\n    created_models = (0...10).map do |i|\n      Parent.new(name: \"model_#{i}\").tap(&.save)\n    end.map(&.id)\n\n    # discard the first two models\n    created_models.shift\n    created_models.shift\n\n    found_models = [] of Int64 | Nil\n\n    Parent.find_each(offset: 2) do |model|\n      found_models << model.id\n    end\n\n    found_models.compact.sort!.should eq created_models.compact\n  end\n\n  it \"doesnt obliterate a parameterized query\" do\n    Parent.clear\n    created_models = (0...10).map do |i|\n      Parent.new(name: \"model_#{i}\").tap(&.save)\n    end.map(&.id)\n\n    looking_for_ids = created_models[0...5]\n\n    found_models = [] of Int64 | Nil\n    Parent.find_each(\"WHERE id IN(#{looking_for_ids.join(\",\")})\") do |model|\n      found_models << model.id\n    end\n\n    found_models.compact.should eq looking_for_ids\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/find_in_batches.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#find_in_batches\" do\n  it \"finds records in batches and yields all the records\" do\n    model_ids = (0...100).map do |i|\n      Parent.new(name: \"model_#{i}\").tap(&.save)\n    end.map(&.id)\n\n    found_models = [] of Int32 | Nil\n    Parent.find_in_batches(batch_size: 10) do |batch|\n      batch.each { |model| found_models << model.id }\n      batch.size.should eq 10\n    end\n\n    found_models.compact.sort!.should eq model_ids.compact\n  end\n\n  it \"doesnt yield when no records are found\" do\n    Parent.find_in_batches do\n      fail \"find_in_batches did yield but shouldn't have\"\n    end\n  end\n\n  it \"errors when batch_size is < 1\" do\n    expect_raises ArgumentError do\n      Parent.find_in_batches batch_size: 0 do\n        fail \"should have raised\"\n      end\n    end\n  end\n\n  it \"returns a small batch when there arent enough results\" do\n    (0...9).each do |i|\n      Parent.new(name: \"model_#{i}\").save\n    end\n\n    Parent.find_in_batches(batch_size: 11) do |batch|\n      batch.size.should eq 9\n    end\n  end\n\n  it \"can start from an offset other than 0\" do\n    created_models = (0...10).map do |i|\n      Parent.new(name: \"model_#{i}\").tap(&.save)\n    end.map(&.id)\n\n    # discard the first two models\n    created_models.shift\n    created_models.shift\n\n    found_models = [] of Int32 | Nil\n\n    Parent.find_in_batches(offset: 2) do |batch|\n      batch.each do |model|\n        found_models << model.id\n      end\n    end\n\n    found_models.compact.sort!.should eq created_models.compact\n  end\n\n  it \"doesnt obliterate a parameterized query\" do\n    created_models = (0...10).map do |i|\n      Parent.new(name: \"model_#{i}\").tap(&.save)\n    end.map(&.id)\n\n    looking_for_ids = created_models[0...5]\n\n    Parent.find_in_batches(\"WHERE id IN(#{looking_for_ids.join(\",\")})\") do |batch|\n      batch.compact_map(&.id).should eq looking_for_ids\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/find_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#find, #find!\" do\n  it \"finds an object by id\" do\n    model = Parent.new\n    model.name = \"Test Comment\"\n    model.save\n\n    found = Parent.find model.id\n    found.should_not be_nil\n    found && (found.id.should eq model.id)\n\n    found = Parent.find! model.id\n    found.id.should eq model.id\n  end\n\n  it \"updates states of new_record and persisted\" do\n    model = Parent.new\n    model.name = \"Test Comment\"\n    model.save\n    model_id = model.id\n\n    model = Parent.find!(model_id)\n    model.new_record?.should be_false\n    model.persisted?.should be_true\n  end\n\n  describe \"with a custom primary key\" do\n    it \"finds the object\" do\n      school = School.new\n      school.name = \"Test School\"\n      school.save\n      primary_key = school.custom_id\n\n      found_school = School.find primary_key\n      found_school.should_not be_nil\n\n      found_school = School.find! primary_key\n      found_school.should be_a(School)\n    end\n  end\n\n  describe \"with a modulized model\" do\n    it \"finds the object\" do\n      county = Nation::County.new\n      county.name = \"Test County\"\n      county.save\n      primary_key = county.id\n\n      found_county = Nation::County.find primary_key\n      found_county.should_not be_nil\n\n      found_county = Nation::County.find! primary_key\n      found_county.should be_a(Nation::County)\n    end\n  end\n\n  it \"returns nil or raises if no result\" do\n    found = Parent.find 0\n    found.should be_nil\n\n    expect_raises(Granite::Querying::NotFound, /No .*Parent.* found where id = 0/) do\n      Parent.find! 0\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/first_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#first, #first!\" do\n  it \"finds the first object\" do\n    Parent.clear\n    first = Parent.new.tap do |model|\n      model.name = \"Test 1\"\n      model.save\n    end\n\n    Parent.new.tap do |model|\n      model.name = \"Test 2\"\n      model.save\n    end\n\n    found = Parent.first\n    if pa = found\n      pa.id.should eq first.id\n    else\n      pa.should_not be_nil\n    end\n\n    found = Parent.first!\n    found.id.should eq first.id\n  end\n\n  it \"supports a SQL clause\" do\n    Parent.clear\n    Parent.new.tap do |model|\n      model.name = \"Test 1\"\n      model.save\n    end\n\n    second = Parent.new.tap do |model|\n      model.name = \"Test 2\"\n      model.save\n    end\n\n    found = Parent.first(\"ORDER BY id DESC\")\n    if pa = found\n      found.id.should eq second.id\n    else\n      pa.should_not be_nil\n    end\n\n    found = Parent.first!(\"ORDER BY id DESC\")\n    found.id.should eq second.id\n  end\n\n  it \"returns nil or raises if no result\" do\n    Parent.clear\n    Parent.new.tap do |model|\n      model.name = \"Test 1\"\n      model.save\n    end\n\n    found = Parent.first(\"WHERE name = 'Test 2'\")\n    found.should be nil\n\n    expect_raises(Granite::Querying::NotFound, /No .*Parent.* found with first\\(WHERE name = 'Test 2'\\)/) do\n      Parent.first!(\"WHERE name = 'Test 2'\")\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/from_rs_spec.cr",
    "content": "require \"../../spec_helper\"\n\nmacro build_review_emitter\n  FieldEmitter.new.tap do |e|\n    e._set_values(\n      [\n        8_i64,\n        \"name\",\n        nil,        # downvotes\n        nil,        # upvotes\n        nil,        # sentiment\n        nil,        # interest\n        true,       # published\n        Time.local, # created_at\n      ]\n    )\n  end\nend\n\ndef method_which_takes_any_model(model : Granite::Base.class)\n  model.as(Granite::Base).from_rs build_review_emitter\nend\n\ndescribe \".from_rs\" do\n  it \"Builds a model from a resultset\" do\n    model = Review.from_rs build_review_emitter\n    model.class.should eq Review\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/passthrough_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#query\" do\n  it \"calls query against the db driver\" do\n    Parent.clear\n    Parent.query \"SELECT name FROM parents\" do |rs|\n      rs.column_name(0).should eq \"name\"\n      rs.close\n    end\n  end\nend\n\ndescribe \"#scalar\" do\n  it \"calls scalar against the db driver\" do\n    Parent.clear\n    Parent.scalar \"SELECT count(*) FROM parents\" do |total|\n      total.should eq 0\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/query_builder_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::Query::BuilderMethods do\n  describe \"#where\" do\n    describe \"with array arguments\" do\n      it \"correctly queries all rows with a list of id values\" do\n        review1 = Review.create(name: \"one\")\n        review2 = Review.create(name: \"two\")\n\n        found = Review.where(id: [review1.id, review2.id]).select\n        found[0].id.should eq review2.id\n        found[1].id.should eq review1.id\n      end\n\n      it \"correctly queries all rows with a list of id values and names\" do\n        review1 = Review.create(name: \"one\")\n        review2 = Review.create(name: \"two\")\n\n        found = Review.where(name: [\"one\", \"two\"]).and(id: [review1.id, review2.id]).select\n        found[0].id.should eq review2.id\n        found[1].id.should eq review1.id\n      end\n\n      it \"correctly queries all rows with a list of id values or names\" do\n        review1 = Review.create(name: \"one\")\n        review2 = Review.create(name: \"two\")\n\n        found = Review.where(id: [1001, 1002]).or(name: [\"one\", \"two\"]).select\n        found[0].id.should eq review2.id\n        found[1].id.should eq review1.id\n\n        found = Review.where(name: [\"one\", \"two\"]).or(id: [1001, 1002]).select\n        found[0].id.should eq review2.id\n        found[1].id.should eq review1.id\n      end\n\n      it \"correctly queries with ids fields which doest exists\" do\n        Review.create(name: \"one\")\n        Review.create(name: \"two\")\n\n        found = Review.where(id: [1001, 1002]).select\n        found.size.should eq 0\n      end\n      it \"correctly queries string fields\" do\n        review1 = Review.create(name: \"one\")\n        review2 = Review.create(name: \"two\")\n\n        found = Review.where(name: [\"one\", \"two\"]).select\n        found[0].id.should eq review2.id\n        found[1].id.should eq review1.id\n      end\n\n      it \"correctly queries number fields\" do\n        Review.clear\n        review1 = Review.create(name: \"one\", downvotes: 99)\n        review2 = Review.create(name: \"two\", downvotes: -4)\n\n        found = Review.where(downvotes: [99, -4]).select\n        found[0].id.should eq review2.id\n        found[1].id.should eq review1.id\n      end\n\n      # Sqlite doesnt have bool literals\n      {% if env(\"CURRENT_ADAPTER\") == \"sqlite\" %}\n        it \"correctly queries bool fields\" do\n          Review.clear\n          Review.create(name: \"one\", published: 1)\n          review2 = Review.create(name: \"two\", published: 0)\n\n          found = Review.where(published: [0]).select\n\n          found.size.should eq 1\n          found[0].id.should eq review2.id\n        end\n      {% else %}\n        it \"correctly queries bool fields\" do\n          Review.clear\n          Review.create(name: \"one\", published: true)\n          review2 = Review.create(name: \"two\", published: false)\n\n          found = Review.where(published: [false]).select\n          found.size.should eq 1\n          found[0].id.should eq review2.id\n        end\n      {% end %}\n    end\n  end\n\n  describe \"#exists?\" do\n    describe \"when there is a record with that ID\" do\n      describe \"when querying on the PK\" do\n        it \"should return true\" do\n          model = Parent.new(name: \"Some Name\")\n          model.save.should be_true\n          Parent.where(id: model.id).exists?.should be_true\n        end\n      end\n\n      describe \"with multiple args\" do\n        it \"should return true\" do\n          model = Parent.new(name: \"Some Name\")\n          model.save.should be_true\n          Parent.where(name: \"Some Name\", id: model.id).exists?.should be_true\n        end\n      end\n    end\n\n    describe \"when there is not a record with that ID\" do\n      describe \"when querying on the PK\" do\n        it \"should return false\" do\n          Parent.where(id: 234567).exists?.should be_false\n        end\n      end\n\n      describe \"with multiple args\" do\n        it \"should return false\" do\n          model = Parent.new(name: \"Some Name\")\n          model.save.should be_true\n          Parent.where(name: \"Some Other Name\", id: model.id).exists?.should be_false\n        end\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/querying/reload_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#reload\" do\n  before_each do\n    Parent.clear\n  end\n\n  it \"reloads the record from the database\" do\n    parent = Parent.create(name: \"Parent\")\n\n    Parent.find!(parent.id).update(name: \"Other\")\n\n    parent.reload.name.should eq \"Other\"\n  end\n\n  it \"raises an error if the record no longer exists\" do\n    parent = Parent.create(name: \"Parent\")\n    parent.destroy\n\n    expect_raises(Granite::Querying::NotFound) do\n      parent.reload\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/select/select_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"custom select\" do\n  before_each do\n    Article.clear\n    Comment.clear\n    EventCon.clear\n  end\n\n  it \"generates custom SQL with the query macro\" do\n    ArticleViewModel.select.should eq \"SELECT articles.id, articles.articlebody, comments.commentbody FROM articles JOIN comments ON comments.articleid = articles.id\"\n  end\n\n  it \"uses custom SQL to populate a view model - #all\" do\n    first = Article.new.tap do |model|\n      model.articlebody = \"The Article Body\"\n      model.save!\n    end\n\n    Comment.new.tap do |model|\n      model.commentbody = \"The Comment Body\"\n      model.articleid = first.id\n      model.save!\n    end\n\n    viewmodel = ArticleViewModel.all\n    viewmodel.first.articlebody.should eq \"The Article Body\"\n    viewmodel.first.commentbody.should eq \"The Comment Body\"\n  end\n\n  it \"allow only selecting specific columns\" do\n    EventCon.create(con_name: \"Con0\", event_name: \"Event0\")\n    EventCon.create(con_name: \"Con1\", event_name: \"Event1\")\n    EventCon.create(con_name: \"Con2\", event_name: \"Event2\")\n\n    EventCon.all.each_with_index do |env, idx|\n      env.id.should be_nil\n      env.con_name.should eq \"Con#{idx}\"\n      env.event_name.should be_nil\n    end\n  end\n\n  # TODO:  `find` on this ViewModel fails because \"id\" is ambiguous in a complex SELECT.\n  # it \"uses custom SQL to populate a view model - #find\" do\n  #   first = Article.new.tap do |model|\n  #     model.articlebody = \"The Article Body\"\n  #     model.save\n  #   end\n\n  #   second = Comment.new.tap do |model|\n  #     model.commentbody = \"The Comment Body\"\n  #     model.articleid = first.id\n  #     model.save\n  #   end\n\n  #   viewmodel = ArticleViewModel.find!(first.id)\n  #   viewmodel.articlebody.should eq \"The Article Body\"\n  #   viewmodel.commentbody.should eq \"The Comment Body\"\n  # end\nend\n"
  },
  {
    "path": "spec/granite/table/table_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::Table do\n  describe \".table_name\" do\n    it \"sets the table name to name specified\" do\n      CustomSongThread.table_name.should eq \"custom_table_name\"\n    end\n\n    it \"sets the table name based on class name if not specified\" do\n      SongThread.table_name.should eq \"song_thread\"\n    end\n\n    it \"strips the namespace when defining the default table now\" do\n      MyApp::Namespace::Model.table_name.should eq \"model\"\n    end\n  end\n\n  describe \".primary_name\" do\n    it \"sets the primary key name to name specified\" do\n      CustomSongThread.primary_name.should eq \"custom_primary_key\"\n    end\n    it \"sets the primary key name to id if not specified\" do\n      SongThread.primary_name.should eq \"id\"\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/transactions/create_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#create\" do\n  it \"creates a new object\" do\n    parent = Parent.create(name: \"Test Parent\")\n    parent.persisted?.should be_true\n    parent.name.should eq(\"Test Parent\")\n  end\n\n  it \"does not create an invalid object\" do\n    parent = Parent.create(name: \"\")\n    parent.persisted?.should be_false\n  end\n\n  describe \"with a custom primary key\" do\n    it \"creates a new object\" do\n      school = School.create(name: \"Test School\")\n      school.persisted?.should be_true\n      school.name.should eq(\"Test School\")\n    end\n  end\n\n  describe \"with a modulized model\" do\n    it \"creates a new object\" do\n      county = Nation::County.create(name: \"Test School\")\n      county.persisted?.should be_true\n      county.name.should eq(\"Test School\")\n    end\n  end\n\n  describe \"using a reserved word as a column name\" do\n    it \"creates a new object\" do\n      reserved_word = ReservedWord.create(all: \"foo\")\n      reserved_word.errors.empty?.should be_true\n      reserved_word.all.should eq(\"foo\")\n    end\n  end\n\n  context \"when skip_timestamps is true\" do\n    it \"does not update the created_at & updated_at fields\" do\n      parent = Parent.create({name: \"new parent\"}, skip_timestamps: true)\n\n      Parent.find!(parent.id).created_at.should be_nil\n      Parent.find!(parent.id).updated_at.should be_nil\n    end\n  end\nend\n\ndescribe \"#create!\" do\n  it \"creates a new object\" do\n    parent = Parent.create!(name: \"Test Parent\")\n    parent.persisted?.should be_true\n    parent.name.should eq(\"Test Parent\")\n  end\n\n  it \"does not save but raise an exception\" do\n    expect_raises(Granite::RecordNotSaved, \"Parent\") do\n      Parent.create!(name: \"\")\n    end\n  end\n\n  context \"when skip_timestamps is true\" do\n    it \"does not update the created_at & updated_at fields\" do\n      parent = Parent.create!({name: \"new parent\"}, skip_timestamps: true)\n\n      Parent.find!(parent.id).created_at.should be_nil\n      Parent.find!(parent.id).updated_at.should be_nil\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/transactions/destroy_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#destroy\" do\n  it \"destroys an object\" do\n    parent = Parent.new\n    parent.name = \"Test Parent\"\n    parent.save\n\n    id = parent.id\n    parent.destroy\n    found = Parent.find id\n    found.should be_nil\n  end\n\n  it \"updates states of destroyed and persisted\" do\n    parent = Parent.new\n    parent.destroyed?.should be_false\n    parent.persisted?.should be_false\n\n    parent.name = \"Test Parent\"\n    parent.save\n    parent.destroyed?.should be_false\n    parent.persisted?.should be_true\n\n    parent.destroy\n    parent.destroyed?.should be_true\n    parent.persisted?.should be_false\n  end\n\n  describe \"with a custom primary key\" do\n    it \"destroys an object\" do\n      school = School.new\n      school.name = \"Test School\"\n      school.save\n      primary_key = school.custom_id\n      school.destroy\n\n      found_school = School.find primary_key\n      found_school.should be_nil\n    end\n  end\n\n  describe \"with a modulized model\" do\n    it \"destroys an object\" do\n      county = Nation::County.new\n      county.name = \"Test County\"\n      county.save\n      primary_key = county.id\n      county.destroy\n\n      found_county = Nation::County.find primary_key\n      found_county.should be_nil\n    end\n  end\nend\n\ndescribe \"#destroy!\" do\n  it \"destroys an object\" do\n    parent = Parent.new\n    parent.name = \"Test Parent\"\n    parent.save!\n\n    id = parent.id\n    parent.destroy\n    found = Parent.find id\n    found.should be_nil\n  end\n\n  it \"does not destroy but raise an exception\" do\n    callback_with_abort = CallbackWithAbort.new\n    callback_with_abort.name = \"DestroyRaisesException\"\n    callback_with_abort.abort_at = \"temp\"\n    callback_with_abort.do_abort = false\n    callback_with_abort.save!\n    callback_with_abort.abort_at = \"before_destroy\"\n    callback_with_abort.do_abort = true\n\n    expect_raises(Granite::RecordNotDestroyed, \"CallbackWithAbort\") do\n      callback_with_abort.destroy!\n    end\n\n    CallbackWithAbort.find_by(name: callback_with_abort.name).should_not be_nil\n  end\nend\n"
  },
  {
    "path": "spec/granite/transactions/import_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#import\" do\n  describe \"using the defualt primary key\" do\n    context \"with an AUTO INCREMENT PK\" do\n      it \"should import 3 new objects\" do\n        Parent.clear\n        to_import = [\n          Parent.new(name: \"ImportParent1\"),\n          Parent.new(name: \"ImportParent2\"),\n          Parent.new(name: \"ImportParent3\"),\n        ]\n        Parent.import(to_import)\n        Parent.all(\"WHERE name LIKE ?\", [\"ImportParent%\"]).size.should eq 3\n      end\n\n      it \"should work with batch_size\" do\n        to_import = [\n          Book.new(name: \"ImportBatchBook1\"),\n          Book.new(name: \"ImportBatchBook2\"),\n          Book.new(name: \"ImportBatchBook3\"),\n          Book.new(name: \"ImportBatchBook4\"),\n        ]\n\n        Book.import(to_import, batch_size: 2)\n        Book.all(\"WHERE name LIKE ?\", [\"ImportBatch%\"]).size.should eq 4\n      end\n\n      it \"should be able to update existing records\" do\n        to_import = [\n          Review.new(name: \"ImportReview1\", published: false, upvotes: 0.to_i64),\n          Review.new(name: \"ImportReview2\", published: false, upvotes: 0.to_i64),\n          Review.new(name: \"ImportReview3\", published: false, upvotes: 0.to_i64),\n          Review.new(name: \"ImportReview4\", published: false, upvotes: 0.to_i64),\n        ]\n\n        Review.import(to_import)\n\n        reviews = Review.all(\"WHERE name LIKE ?\", [\"ImportReview%\"])\n        reviews.size.should eq 4\n        reviews.none?(&.published).should be_true\n        reviews.all? { |r| r.upvotes == 0 }.should be_true\n\n        reviews.each { |r| r.published = true; r.upvotes = 1.to_i64 }\n\n        Review.import(reviews, update_on_duplicate: true, columns: [\"published\", \"upvotes\"])\n\n        reviews = Review.all(\"WHERE name LIKE ?\", [\"ImportReview%\"])\n\n        reviews.size.should eq 4\n        reviews.all?(&.published).should be_true\n        reviews.all? { |r| r.upvotes == 1 }.should be_true\n      end\n    end\n    context \"with non AUTO INCREMENT PK\" do\n      it \"should work with on_duplicate_key_update\" do\n        to_import = [\n          NonAutoDefaultPK.new(id: 1.to_i64, name: \"NonAutoDefaultPK1\"),\n          NonAutoDefaultPK.new(id: 2.to_i64, name: \"NonAutoDefaultPK2\"),\n          NonAutoDefaultPK.new(id: 3.to_i64, name: \"NonAutoDefaultPK3\"),\n        ]\n\n        NonAutoDefaultPK.import(to_import)\n\n        to_import = [\n          NonAutoDefaultPK.new(id: 3.to_i64, name: \"NonAutoDefaultPK3\"),\n        ]\n\n        NonAutoDefaultPK.import(to_import, update_on_duplicate: true, columns: [\"name\"])\n\n        record = NonAutoDefaultPK.find! 3.to_i64\n        record.name.should eq \"NonAutoDefaultPK3\"\n        record.id.should eq 3.to_i64\n      end\n\n      it \"should work with on_duplicate_key_ignore\" do\n        to_import = [\n          NonAutoDefaultPK.new(id: 4.to_i64, name: \"NonAutoDefaultPK4\"),\n          NonAutoDefaultPK.new(id: 5.to_i64, name: \"NonAutoDefaultPK5\"),\n          NonAutoDefaultPK.new(id: 6.to_i64, name: \"NonAutoDefaultPK6\"),\n        ]\n\n        NonAutoDefaultPK.import(to_import)\n\n        to_import = [\n          NonAutoDefaultPK.new(id: 6.to_i64, name: \"NonAutoDefaultPK6\"),\n        ]\n\n        NonAutoDefaultPK.import(to_import, ignore_on_duplicate: true)\n\n        record = NonAutoDefaultPK.find! 6.to_i64\n        record.name.should eq \"NonAutoDefaultPK6\"\n        record.id.should eq 6.to_i64\n      end\n    end\n  end\n  describe \"using a custom primary key\" do\n    context \"with an AUTO INCREMENT PK\" do\n      it \"should import 3 new objects\" do\n        to_import = [\n          School.new(name: \"ImportBasicSchool1\"),\n          School.new(name: \"ImportBasicSchool2\"),\n          School.new(name: \"ImportBasicSchool3\"),\n        ]\n        School.import(to_import)\n        School.all(\"WHERE name LIKE ?\", [\"ImportBasicSchool%\"]).size.should eq 3\n      end\n\n      it \"should work with batch_size\" do\n        to_import = [\n          School.new(name: \"ImportBatchSchool1\"),\n          School.new(name: \"ImportBatchSchool2\"),\n          School.new(name: \"ImportBatchSchool3\"),\n          School.new(name: \"ImportBatchSchool4\"),\n        ]\n\n        School.import(to_import, batch_size: 2)\n        School.all(\"WHERE name LIKE ?\", [\"ImportBatchSchool%\"]).size.should eq 4\n      end\n\n      it \"should be able to update existing records\" do\n        to_import = [\n          School.new(name: \"ImportExistingSchool\"),\n          School.new(name: \"ImportExistingSchool\"),\n          School.new(name: \"ImportExistingSchool\"),\n          School.new(name: \"ImportExistingSchool\"),\n        ]\n\n        School.import(to_import)\n\n        schools = School.all(\"WHERE name = ?\", [\"ImportExistingSchool\"])\n        schools.size.should eq 4\n        schools.all? { |s| s.name == \"ImportExistingSchool\" }.should be_true\n\n        schools.each(&.name=(\"ImportExistingSchoolEdited\"))\n\n        School.import(schools, update_on_duplicate: true, columns: [\"name\"])\n\n        schools = School.all(\"WHERE name LIKE ?\", [\"ImportExistingSchool%\"])\n        schools.size.should eq 4\n        schools.all? { |s| s.name == \"ImportExistingSchoolEdited\" }.should be_true\n      end\n    end\n    context \"with non AUTO INCREMENT PK\" do\n      it \"should work with on_duplicate_key_update\" do\n        to_import = [\n          NonAutoCustomPK.new(custom_id: 1.to_i64, name: \"NonAutoCustomPK1\"),\n          NonAutoCustomPK.new(custom_id: 2.to_i64.to_i64, name: \"NonAutoCustomPK2\"),\n          NonAutoCustomPK.new(custom_id: 3.to_i64, name: \"NonAutoCustomPK3\"),\n        ]\n\n        NonAutoCustomPK.import(to_import)\n\n        to_import = [\n          NonAutoCustomPK.new(custom_id: 3.to_i64, name: \"NonAutoCustomPK3\"),\n        ]\n\n        NonAutoCustomPK.import(to_import, update_on_duplicate: true, columns: [\"name\"])\n\n        record = NonAutoCustomPK.find! 3.to_i64\n        record.name.should eq \"NonAutoCustomPK3\"\n        record.custom_id.should eq 3.to_i64\n      end\n\n      it \"should work with on_duplicate_key_ignore\" do\n        to_import = [\n          NonAutoCustomPK.new(custom_id: 4.to_i64, name: \"NonAutoCustomPK4\"),\n          NonAutoCustomPK.new(custom_id: 5.to_i64, name: \"NonAutoCustomPK5\"),\n          NonAutoCustomPK.new(custom_id: 6.to_i64, name: \"NonAutoCustomPK6\"),\n        ]\n\n        NonAutoCustomPK.import(to_import)\n\n        to_import = [\n          NonAutoCustomPK.new(custom_id: 6.to_i64, name: \"NonAutoCustomPK6\"),\n        ]\n\n        NonAutoCustomPK.import(to_import, ignore_on_duplicate: true)\n\n        record = NonAutoCustomPK.find! 6.to_i64\n        record.name.should eq \"NonAutoCustomPK6\"\n        record.custom_id.should eq 6.to_i64\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/transactions/save_natural_key_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"(Natural Key) #save\" do\n  it \"fails when a primary key is not set\" do\n    kv = Kvs.new\n    kv.save.should be_false\n    kv.errors.first.message.should eq \"Primary key('k') cannot be null\"\n  end\n\n  it \"creates a new object when a primary key is given\" do\n    kv = Kvs.new\n    kv.k = \"foo\"\n    kv.save.should be_true\n\n    kv = Kvs.find!(\"foo\")\n    kv.k.should eq(\"foo\")\n  end\n\n  it \"updates an existing object\" do\n    kv = Kvs.new\n    kv.k = \"foo2\"\n    kv.v = \"1\"\n    kv.save.should be_true\n\n    kv.v = \"2\"\n    kv.save.should be_true\n    kv.k.should eq(\"foo2\")\n    kv.v.should eq(\"2\")\n  end\nend\n\ndescribe \"(Natural Key) usecases\" do\n  it \"CRUD\" do\n    Kvs.clear\n\n    # # Create\n    port = Kvs.new(k: \"mysql_port\", v: \"3306\")\n    port.new_record?.should be_true\n    port.save.should be_true\n    port.v.should eq(\"3306\")\n    Kvs.count.should eq(1)\n\n    # # Read\n    port = Kvs.find!(\"mysql_port\")\n    port.v.should eq(\"3306\")\n    port.new_record?.should be_false\n\n    # # Update\n    port.v = \"3307\"\n    port.new_record?.should be_false\n    port.save.should be_true\n    port.v.should eq(\"3307\")\n    Kvs.count.should eq(1)\n\n    # # Delete\n    port.destroy.should be_true\n    Kvs.count.should eq(0)\n  end\nend\n"
  },
  {
    "path": "spec/granite/transactions/save_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#save\" do\n  it \"creates a new object\" do\n    parent = Parent.new\n    parent.name = \"Test Parent\"\n    parent.save\n    parent.persisted?.should be_true\n  end\n\n  it \"does not create an invalid object\" do\n    parent = Parent.new\n    parent.name = \"\"\n    parent.save\n    parent.persisted?.should be_false\n  end\n\n  it \"create an invalid object with validation disabled\" do\n    parent = Parent.new\n    parent.name = \"\"\n    parent.save(validate: false)\n    parent.persisted?.should be_true\n  end\n\n  it \"does not create an invalid object with validation explicitly enabled\" do\n    parent = Parent.new\n    parent.name = \"\"\n    parent.save(validate: true)\n    parent.persisted?.should be_false\n  end\n\n  it \"does not save a model with type conversion errors\" do\n    model = Comment.new(articleid: \"foo\")\n    model.errors.size.should eq 1\n    model.save.should be_false\n  end\n\n  it \"does not update timestamps is skip_timestamps is true\" do\n    time = Time.utc(2023, 9, 1)\n    parent = Parent.new\n    parent.name = \"Test Parent\"\n    parent.created_at = time\n    parent.updated_at = time\n    parent.save(skip_timestamps: true)\n    parent.created_at.should eq time\n    parent.updated_at.should eq time\n  end\n\n  it \"updates an existing object\" do\n    Parent.clear\n    parent = Parent.new\n    parent.name = \"Test Parent\"\n    parent.save\n    parent.name = \"Test Parent 2\"\n    parent.save\n\n    parents = Parent.all\n    parents.size.should eq 1\n\n    found = Parent.first!\n    found.name.should eq parent.name\n  end\n\n  it \"does not update an invalid object\" do\n    parent = Parent.new\n    parent.name = \"Test Parent\"\n    parent.save\n    parent.name = \"\"\n    parent.save\n    parent = Parent.find! parent.id\n    parent.name.should eq \"Test Parent\"\n  end\n\n  it \"update an invalid object with validation disabled\" do\n    Parent.clear\n    parent = Parent.new\n    parent.name = \"Test Parent\"\n    parent.save\n    parent.name = \"\"\n    parent.save(validate: false)\n\n    parents = Parent.all\n    parents.size.should eq 1\n\n    found = Parent.first!\n    found.name.should eq parent.name\n  end\n\n  it \"does not update an invalid object with validation explicitly enabled\" do\n    parent = Parent.new\n    parent.name = \"Test Parent\"\n    parent.save\n    parent.name = \"\"\n    parent.save(validate: true)\n    parent = Parent.find! parent.id\n    parent.name.should eq \"Test Parent\"\n  end\n\n  it \"does not update when the conflicted primary key is given to the new record\" do\n    parent1 = Parent.new\n    parent1.name = \"Test Parent\"\n    parent1.save.should be_true\n\n    parent2 = Parent.new\n    parent2.id = parent1.id\n    parent2.name = \"Test Parent2\"\n    parent2.save.should be_false\n  end\n\n  describe \"with a custom primary key\" do\n    it \"creates a new object\" do\n      school = School.new\n      school.name = \"Test School\"\n      school.save\n      school.custom_id.should_not be_nil\n    end\n\n    it \"updates an existing object\" do\n      old_name = \"Test School 1\"\n      new_name = \"Test School 2\"\n\n      school = School.new\n      school.name = old_name\n      school.save\n\n      primary_key = school.custom_id\n\n      school.name = new_name\n      school.save\n\n      found_school = School.find! primary_key\n      found_school.custom_id.should eq primary_key\n      found_school.name.should eq new_name\n    end\n\n    it \"updates states of new_record and persisted\" do\n      parent = Parent.new\n      parent.new_record?.should be_true\n      parent.persisted?.should be_false\n\n      parent.name = \"Test Parent\"\n      parent.save\n      parent.new_record?.should be_false\n      parent.persisted?.should be_true\n    end\n  end\n\n  describe \"with a modulized model\" do\n    it \"creates a new object\" do\n      county = Nation::County.new\n      county.name = \"Test School\"\n      county.save\n      county.persisted?.should be_true\n    end\n\n    it \"updates an existing object\" do\n      old_name = \"Test County 1\"\n      new_name = \"Test County 2\"\n\n      county = Nation::County.new\n      county.name = old_name\n      county.save\n\n      primary_key = county.id\n\n      county.name = new_name\n      county.save\n\n      found_county = Nation::County.find! primary_key\n      found_county.name.should eq new_name\n    end\n  end\n\n  describe \"using a reserved word as a column name\" do\n    # `all` is a reserved word in almost RDB like MySQL, PostgreSQL\n    it \"creates and updates\" do\n      reserved_word = ReservedWord.new\n      reserved_word.all = \"foo\"\n      reserved_word.save\n      reserved_word.errors.empty?.should be_true\n\n      reserved_word.all = \"bar\"\n      reserved_word.save\n      reserved_word.errors.empty?.should be_true\n      reserved_word.all.should eq(\"bar\")\n    end\n  end\nend\n\ndescribe \"#save!\" do\n  it \"creates a new object\" do\n    parent = Parent.new\n    parent.name = \"Test Parent\"\n    parent.save!\n    parent.persisted?.should be_true\n  end\n\n  it \"does not create but raise an exception\" do\n    parent = Parent.new\n\n    expect_raises(Granite::RecordNotSaved, \"Parent\") do\n      parent.save!\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/transactions/touch_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#touch\" do\n  it \"should raise on new record\" do\n    expect_raises Exception, \"Cannot touch on a new record object\" { TimeTest.new.touch }\n  end\n\n  it \"should raise on non existent field\" do\n    expect_raises Exception, \"Field 'foo' does not exist on type 'TimeTest'.\" do\n      model = TimeTest.create(name: \"foo\")\n      model.touch(:foo)\n    end\n  end\n\n  it \"should raise on non `Time` field\" do\n    expect_raises Exception, \"TimeTest.name cannot be touched.  It is not of type `Time`.\" do\n      model = TimeTest.create(name: \"foo\")\n      model.touch(:name)\n    end\n  end\n\n  it \"updates updated_at on an object\" do\n    old_time = Time.utc.at_beginning_of_second\n    object = TimeTest.create(test: old_time)\n\n    sleep 3\n\n    new_time = Time.utc.at_beginning_of_second\n    object.touch\n\n    object.updated_at.should eq new_time\n    object.test.should eq old_time\n    object.created_at.should eq old_time\n  end\n\n  it \"updates updated_at + custom fields on an object\" do\n    old_time = Time.utc.at_beginning_of_second\n    object = TimeTest.create(test: old_time)\n\n    sleep 3\n\n    new_time = Time.utc.at_beginning_of_second\n    object.touch(\"test\")\n\n    object.updated_at.should eq new_time\n    object.test.should eq new_time\n    object.created_at.should eq old_time\n  end\nend\n"
  },
  {
    "path": "spec/granite/transactions/update_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe \"#update\" do\n  it \"updates an object\" do\n    parent = Parent.new(name: \"New Parent\")\n    parent.save!\n\n    parent.update(name: \"Other parent\").should be_true\n    parent.name.should eq \"Other parent\"\n\n    Parent.find!(parent.id).name.should eq \"Other parent\"\n  end\n\n  it \"allows setting a value to nil\" do\n    model = Teacher.create!(name: \"New Parent\")\n\n    model.update(name: nil)\n\n    model.name.should be_nil\n\n    Teacher.find!(model.id).name.should be_nil\n  end\n\n  it \"does not update an invalid object\" do\n    parent = Parent.new(name: \"New Parent\")\n    parent.save!\n\n    parent.update(name: \"\").should be_false\n    parent.name.should eq \"\"\n\n    Parent.find!(parent.id).name.should eq \"New Parent\"\n  end\n\n  context \"when created_at is nil\" do\n    it \"does not update created_at\" do\n      parent = Parent.new(name: \"New Parent\")\n      parent.save!\n\n      created_at = parent.created_at!.at_beginning_of_second\n\n      # Simulating instantiating a new object with same ID\n      new_parent = Parent.new(name: \"New New Parent\")\n      new_parent.id = parent.id\n      new_parent.new_record = false\n      new_parent.updated_at = parent.updated_at\n      new_parent.save!\n\n      saved_parent = Parent.find!(parent.id)\n      saved_parent.name.should eq \"New New Parent\"\n      saved_parent.created_at.should eq created_at\n      saved_parent.updated_at.should eq Time.utc.at_beginning_of_second\n    end\n  end\n\n  context \"when skip_timestamps is true\" do\n    it \"does not update the updated_at field\" do\n      time = Time.utc(2023, 9, 1)\n      parent = Parent.create(name: \"New Parent\")\n      parent.updated_at = time\n      parent.update({name: \"Other Parent\"}, skip_timestamps: true)\n\n      Parent.find!(parent.id).updated_at.should eq time\n    end\n  end\nend\n\ndescribe \"#update!\" do\n  it \"updates an object\" do\n    parent = Parent.new(name: \"New Parent\")\n    parent.save!\n\n    parent.update!(name: \"Other parent\")\n    parent.name.should eq \"Other parent\"\n\n    Parent.find!(parent.id).name.should eq \"Other parent\"\n  end\n\n  it \"does not update but raises an exception\" do\n    parent = Parent.new(name: \"New Parent\")\n    parent.save!\n\n    expect_raises(Granite::RecordNotSaved, \"Parent\") do\n      parent.update!(name: \"\")\n    end\n\n    Parent.find!(parent.id).name.should eq \"New Parent\"\n  end\n\n  context \"when skip_timestamps is true\" do\n    it \"does not update the updated_at field\" do\n      time = Time.utc(2023, 9, 1)\n      parent = Parent.create(name: \"New Parent\")\n      parent.updated_at = time\n      parent.update!({name: \"Other Parent\"}, skip_timestamps: true)\n\n      Parent.find!(parent.id).updated_at.should eq time\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/validation_helpers/blank_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n  context \"Blank\" do\n    it \"should work for is_blank and not_blank\" do\n      blank_test = Validators::BlankTest.new\n\n      blank_test.first_name_not_blank = \"\"\n      blank_test.last_name_not_blank = \"      \"\n\n      blank_test.first_name_is_blank = \"foo\"\n      blank_test.last_name_is_blank = \"  bar  \"\n\n      blank_test.save\n\n      blank_test.errors.size.should eq 4\n      blank_test.errors[0].message.should eq \"first_name_not_blank must not be blank\"\n      blank_test.errors[1].message.should eq \"last_name_not_blank must not be blank\"\n      blank_test.errors[2].message.should eq \"first_name_is_blank must be blank\"\n      blank_test.errors[3].message.should eq \"last_name_is_blank must be blank\"\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/validation_helpers/choice_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n  context \"Choice\" do\n    it \"should work for is_valid_choice\" do\n      choice_test = Validators::ChoiceTest.new\n\n      choice_test.number_symbol = 4\n      choice_test.type_array_symbol = \"foo\"\n\n      choice_test.number_string = 2\n      choice_test.type_array_string = \"bar\"\n\n      choice_test.save\n\n      choice_test.errors.size.should eq 4\n      choice_test.errors[0].message.should eq \"number_symbol has an invalid choice. Valid choices are: 1,2,3\"\n      choice_test.errors[1].message.should eq \"type_array_symbol has an invalid choice. Valid choices are: internal,external,third_party\"\n      choice_test.errors[2].message.should eq \"number_string has an invalid choice. Valid choices are: 4,5,6\"\n      choice_test.errors[3].message.should eq \"type_array_string has an invalid choice. Valid choices are: internal,external,third_party\"\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/validation_helpers/exclusion_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n  context \"Exclusion\" do\n    it \"should allow non reserved words\" do\n      exclusion = Validators::ExclusionTest.new\n      exclusion.name = \"none_conflicting\"\n      exclusion.save\n\n      exclusion.errors.size.should eq 0\n    end\n\n    it \"should disallow reservered words\" do\n      exclusion = Validators::ExclusionTest.new\n      exclusion.name = \"test_name\"\n      exclusion.save\n\n      exclusion.errors.size.should eq 1\n      exclusion.errors[0].message.should eq \"Name got reserved values. Reserved values are test_name\"\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/validation_helpers/inequality_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n  context \"Less Than\" do\n    it \"should work for less_than\" do\n      less_than_test = Validators::LessThanTest.new\n\n      less_than_test.int_32_lt = 10\n      less_than_test.float_32_lt = 20.5.to_f32\n\n      less_than_test.int_32_lte = 52\n      less_than_test.float_32_lte = 155.55.to_f32\n\n      less_than_test.save\n\n      less_than_test.errors.size.should eq 4\n      less_than_test.errors[0].message.should eq \"int_32_lt must be less than 10\"\n      less_than_test.errors[1].message.should eq \"float_32_lt must be less than 20.5\"\n      less_than_test.errors[2].message.should eq \"int_32_lte must be less than or equal to 50\"\n      less_than_test.errors[3].message.should eq \"float_32_lte must be less than or equal to 100.25\"\n\n      less_than_test_nil = Validators::LessThanTest.new\n\n      expect_raises(Exception, \"Nil assertion failed\") do\n        less_than_test_nil.save\n      end\n    end\n  end\n\n  context \"Greater Than\" do\n    it \"should work for greater_than\" do\n      greater_than_test = Validators::GreaterThanTest.new\n\n      greater_than_test.int_32_lt = 10\n      greater_than_test.float_32_lt = 20.5.to_f32\n\n      greater_than_test.int_32_lte = 49\n      greater_than_test.float_32_lte = 100.20.to_f32\n\n      greater_than_test.save\n\n      greater_than_test.errors.size.should eq 4\n      greater_than_test.errors[0].message.should eq \"int_32_lt must be greater than 10\"\n      greater_than_test.errors[1].message.should eq \"float_32_lt must be greater than 20.5\"\n      greater_than_test.errors[2].message.should eq \"int_32_lte must be greater than or equal to 50\"\n      greater_than_test.errors[3].message.should eq \"float_32_lte must be greater than or equal to 100.25\"\n\n      greater_than_test = Validators::GreaterThanTest.new\n\n      expect_raises(Exception, \"Nil assertion failed\") do\n        greater_than_test.save\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/validation_helpers/lenght_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n  context \"Length\" do\n    it \"should work for length\" do\n      length_test = Validators::LengthTest.new\n\n      length_test.title = \"one\"\n      length_test.description = \"abcdefghijklmnopqrstuvwxyz\"\n\n      length_test.save\n\n      length_test.errors.size.should eq 2\n      length_test.errors[0].message.should eq \"title is too short. It must be at least 5\"\n      length_test.errors[1].message.should eq \"description is too long. It must be at most 25\"\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/validation_helpers/nil_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n  context \"Nil\" do\n    it \"should work for is_nil and not_nil for all data types\" do\n      nil_test = Validators::NilTest.new\n\n      nil_test.first_name = \"John\"\n      nil_test.last_name = \"Smith\"\n      nil_test.age = 32\n      nil_test.born = true\n      nil_test.value = 123.56.to_f32\n\n      nil_test.save\n\n      nil_test.errors.size.should eq 10\n      nil_test.errors[0].message.should eq \"first_name_not_nil must not be nil\"\n      nil_test.errors[1].message.should eq \"last_name_not_nil must not be nil\"\n      nil_test.errors[2].message.should eq \"age_not_nil must not be nil\"\n      nil_test.errors[3].message.should eq \"born_not_nil must not be nil\"\n      nil_test.errors[4].message.should eq \"value_not_nil must not be nil\"\n      nil_test.errors[5].message.should eq \"first_name must be nil\"\n      nil_test.errors[6].message.should eq \"last_name must be nil\"\n      nil_test.errors[7].message.should eq \"age must be nil\"\n      nil_test.errors[8].message.should eq \"born must be nil\"\n      nil_test.errors[9].message.should eq \"value must be nil\"\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/validation_helpers/uniqueness_spec.cr",
    "content": "require \"../../spec_helper\"\n\ndescribe Granite::ValidationHelpers do\n  context \"Uniqueness\" do\n    before_each do\n      Validators::PersonUniqueness.migrator.drop_and_create\n    end\n\n    it \"should work for uniqueness\" do\n      person_uniqueness1 = Validators::PersonUniqueness.new\n      person_uniqueness2 = Validators::PersonUniqueness.new\n\n      person_uniqueness1.name = \"awesomeName\"\n      person_uniqueness2.name = \"awesomeName\"\n\n      person_uniqueness1.save\n      person_uniqueness2.save\n\n      person_uniqueness1.errors.size.should eq 0\n      person_uniqueness2.errors.size.should eq 1\n\n      person_uniqueness2.errors[0].message.should eq \"name should be unique\"\n    end\n\n    it \"should work for uniqueness on the same instance\" do\n      person_uniqueness1 = Validators::PersonUniqueness.new\n\n      person_uniqueness1.name = \"awesomeName\"\n      person_uniqueness1.save\n\n      person_uniqueness1.errors.size.should eq 0\n\n      person_uniqueness1.name = \"awesomeName\"\n      person_uniqueness1.save\n\n      person_uniqueness1.errors.size.should eq 0\n    end\n  end\nend\n"
  },
  {
    "path": "spec/granite/validations/validator_spec.cr",
    "content": "require \"../../spec_helper\"\n\n{% begin %}\n  {% adapter_literal = env(\"CURRENT_ADAPTER\").id %}\n  class NameTest < Granite::Base\n    connection {{ adapter_literal }}\n\n    column id : Int64, primary: true\n    column name : String?\n\n    validate :name, \"cannot be blank\", ->(s : NameTest) do\n      !s.name.to_s.blank?\n    end\n  end\n\n  class EmailTest < Granite::Base\n    connection {{ adapter_literal }}\n\n    column id : Int64, primary: true\n    column email : String?\n\n    validate :email, \"cannot be blank\" do |email_test|\n      !email_test.email.to_s.blank?\n    end\n  end\n\n  class PasswordTest < Granite::Base\n    connection {{ adapter_literal }}\n\n    column id : Int64, primary: true\n    column password : String?\n    column password_validation : String?\n\n    validate \"password and validation should match\" do |password_test|\n      password_test.password == password_test.password_validation\n    end\n  end\n\n  describe Granite::Validators do\n    describe \"validates using proc\" do\n      it \"returns true if name is set\" do\n        subject = NameTest.new\n        subject.name = \"name\"\n        subject.valid?.should eq true\n      end\n\n      it \"returns false if name is blank\" do\n        subject = NameTest.new\n        subject.name = \"\"\n        subject.valid?.should eq false\n      end\n    end\n\n    describe \"validates using block\" do\n      it \"returns true if email is set\" do\n        subject = EmailTest.new\n        subject.email = \"test@example.com\"\n        subject.valid?.should eq true\n      end\n\n      it \"returns false if email is blank\" do\n        subject = EmailTest.new\n        subject.email = \"\"\n        subject.valid?.should eq false\n      end\n    end\n\n    describe \"validates using block without field\" do\n      it \"returns true if passwords match\" do\n        subject = PasswordTest.new\n        subject.password = \"123\"\n        subject.password_validation = \"123\"\n        subject.valid?.should eq true\n      end\n\n      it \"returns false if password does not match\" do\n        subject = PasswordTest.new\n        subject.password = \"123\"\n        subject.password_validation = \"1234\"\n        subject.valid?.should eq false\n      end\n    end\n\n    describe \"validates cleanly after previously failing\" do\n      it \"returns true if name is rectified after after failing\" do\n        subject = NameTest.new\n        subject.name = \"\"\n        subject.valid?.should eq false\n\n        subject.name = \"name\"\n        subject.valid?.should eq true\n      end\n    end\n  end\n{% end %}\n"
  },
  {
    "path": "spec/granite_spec.cr",
    "content": "require \"./spec_helper\"\n\nclass SomeClass\n  def initialize(@model_class : Granite::Base.class); end\n\n  def valid? : Bool\n    @model_class.exists? 123\n  end\n\n  def table : String\n    @model_class.table_name\n  end\nend\n\ndescribe Granite::Base do\n  it \"class methods should work when type restricted to `Granite::Base`\" do\n    f = SomeClass.new(Teacher)\n    f.valid?.should be_false\n    f.table.should eq \"teachers\"\n  end\n\n  it \"should allow false as a column value\" do\n    model = BoolModel.create active: false\n\n    model.active.should be_false\n    model.id.should eq 1\n\n    fetched_model = BoolModel.find! model.id\n    fetched_model.active.should be_false\n  end\n\n  describe \"instantiation\" do\n    describe \"with default values\" do\n      it \"should instaniate correctly\" do\n        model = DefaultValues.new\n        model.name.should eq \"Jim\"\n        model.age.should eq 0.0\n        model.is_alive.should be_true\n      end\n    end\n\n    describe \"with a named tuple\" do\n      it \"should instaniate correctly\" do\n        model = DefaultValues.new name: \"Fred\", is_alive: false\n        model.name.should eq \"Fred\"\n        model.age.should eq 0.0\n        model.is_alive.should be_false\n      end\n    end\n\n    describe \"with a hash\" do\n      it \"should instaniate correctly\" do\n        model = DefaultValues.new({\"name\" => \"Bob\", \"age\" => 3.14})\n        model.name.should eq \"Bob\"\n        model.age.should eq 3.14\n        model.is_alive.should be_true\n      end\n    end\n\n    describe \"with a UUID\" do\n      it \"should instaniate correctly\" do\n        uuid = UUID.random\n        model = UUIDNaturalModel.new uuid: uuid, field_uuid: uuid\n        model.uuid.should be_a UUID?\n        model.field_uuid.should be_a UUID?\n        model.uuid.should eq uuid\n        model.field_uuid.should eq uuid\n      end\n    end\n\n    describe \"with string numeric values\" do\n      it \"should instaniate correctly\" do\n        model = StringConversion.new({\"user_id\" => \"1\", \"int32\" => \"17\", \"float32\" => \"3.14\", \"float\" => \"92342.2342342\"})\n\n        model.user_id.should be_a Int64\n        model.user_id.should eq 1\n\n        model.int32.should be_a Int32\n        model.int32.should eq 17\n\n        model.float32.should be_a Float32\n        model.float32.should eq 3.14_f32\n\n        model.float.should be_a Float64\n        model.float.should eq 92342.2342342\n      end\n    end\n  end\n\n  describe Log do\n    it \"should be logged as DEBUG\" do\n      backend = Log::MemoryBackend.new\n\n      Log.builder.bind \"granite\", :debug, backend\n\n      Person.first\n\n      backend.entries.first.severity.debug?.should be_true\n      backend.entries.first.message.should match /.*SELECT.*people.*id.*FROM.*people.*LIMIT.*1.*: .*\\[\\]/\n    end\n\n    it \"should not be logged\" do\n      a = 0\n      Log.for(\"granite.test\").info { a = 1 }\n      a.should eq 0\n    end\n  end\n\n  describe \"JSON\" do\n    describe \".from_json\" do\n      it \"can create an object from json\" do\n        json_str = %({\"name\": \"json::anyReview\",\"upvotes\": 2, \"sentiment\": 1.23, \"interest\": 4.56, \"published\": true})\n\n        review = Review.from_json(json_str)\n        review.name.should eq \"json::anyReview\"\n        review.upvotes.should eq 2\n        review.sentiment.should eq 1.23_f32\n        review.interest.should eq 4.56\n        review.published.should eq true\n        review.created_at.should be_nil\n      end\n\n      it \"can create an array of objects from json\" do\n        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}])\n\n        review = Array(Review).from_json(json_str)\n        review[0].name.should eq \"json1\"\n        review[0].upvotes.should eq 2\n        review[0].sentiment.should eq 1.23_f32\n        review[0].interest.should eq 4.56\n        review[0].published.should be_true\n        review[0].created_at.should be_nil\n\n        review[1].name.should eq \"json2\"\n        review[1].upvotes.should eq 0\n        review[1].sentiment.should eq 5.00_f32\n        review[1].interest.should eq 6.99\n        review[1].published.should be_false\n        review[1].created_at.should be_nil\n      end\n\n      it \"works with after_initialize\" do\n        model = AfterInit.from_json(%({\"name\": \"after_initialize\"}))\n\n        model.name.should eq \"after_initialize\"\n        model.priority.should eq 1000\n      end\n\n      describe \"with default values\" do\n        it \"correctly applies values\" do\n          model = DefaultValues.from_json(%({\"name\": \"Bob\"}))\n          model.name.should eq \"Bob\"\n          model.age.should eq 0.0\n          model.is_alive.should be_true\n        end\n      end\n    end\n\n    describe \"#to_json\" do\n      it \"emits nil values when told\" do\n        t = TodoEmitNull.new(name: \"test todo\", priority: 20)\n        result = %({\"id\":null,\"name\":\"test todo\",\"priority\":20,\"created_at\":null,\"updated_at\":null})\n\n        t.to_json.should eq result\n      end\n\n      it \"does not emit nil values by default\" do\n        t = Todo.new(name: \"test todo\", priority: 20)\n        result = %({\"name\":\"test todo\",\"priority\":20})\n\n        t.to_json.should eq result\n      end\n\n      it \"works with array of models\" do\n        todos = [\n          Todo.new(name: \"todo 1\", priority: 1),\n          Todo.new(name: \"todo 2\", priority: 2),\n          Todo.new(name: \"todo 3\", priority: 3),\n        ]\n\n        collection = todos.to_json\n        collection.should eq %([{\"name\":\"todo 1\",\"priority\":1},{\"name\":\"todo 2\",\"priority\":2},{\"name\":\"todo 3\",\"priority\":3}])\n      end\n    end\n\n    context \"with json_options\" do\n      model = TodoJsonOptions.from_json(%({\"task_name\": \"The Task\", \"priority\": 9000}))\n      it \"should deserialize correctly\" do\n        model.name.should eq \"The Task\"\n        model.priority.should be_nil\n      end\n\n      it \"should serialize correctly\" do\n        model.to_json.should eq %({\"task_name\":\"The Task\"})\n      end\n\n      describe \"when using timestamp fields\" do\n        TodoJsonOptions.import([\n          TodoJsonOptions.new(name: \"first todo\", priority: 200),\n          TodoJsonOptions.new(name: \"second todo\", priority: 500),\n          TodoJsonOptions.new(name: \"third todo\", priority: 300),\n        ])\n\n        it \"should serialize correctly\" do\n          todos = TodoJsonOptions.order(id: :asc).select\n          todos[0].to_json.should eq %({\"id\":1,\"task_name\":\"first todo\",\"posted\":\"#{Time::Format::RFC_3339.format(todos[0].created_at!)}\"})\n          todos[1].to_json.should eq %({\"id\":2,\"task_name\":\"second todo\",\"posted\":\"#{Time::Format::RFC_3339.format(todos[1].created_at!)}\"})\n          todos[2].to_json.should eq %({\"id\":3,\"task_name\":\"third todo\",\"posted\":\"#{Time::Format::RFC_3339.format(todos[2].created_at!)}\"})\n        end\n      end\n    end\n  end\n\n  describe \"YAML\" do\n    describe \".from_yaml\" do\n      it \"can create an object from YAML\" do\n        yaml_str = %(---\\nname: yaml::anyReview\\nupvotes: 2\\nsentiment: 1.23\\ninterest: 4.56\\npublished: true)\n\n        review = Review.from_yaml(yaml_str)\n        review.name.should eq \"yaml::anyReview\"\n        review.upvotes.should eq 2\n        review.sentiment.should eq 1.23.to_f32\n        review.interest.should eq 4.56\n        review.published.should eq true\n        review.created_at.should be_nil\n      end\n\n      it \"can create an array of objects from YAML\" do\n        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\"\n\n        review = Array(Review).from_yaml(yaml_str)\n        review[0].name.should eq \"yaml1\"\n        review[0].upvotes.should eq 2\n        review[0].sentiment.should eq 1.23.to_f32\n        review[0].interest.should eq 4.56\n        review[0].published.should be_true\n        review[0].created_at.should be_nil\n\n        review[1].name.should eq \"yaml2\"\n        review[1].upvotes.should eq 0\n        review[1].sentiment.should eq 5.00.to_f32\n        review[1].interest.should eq 6.99\n        review[1].published.should be_false\n        review[1].created_at.should be_nil\n      end\n\n      it \"works with after_initialize\" do\n        model = AfterInit.from_yaml(%(---\\nname: after_initialize))\n\n        model.name.should eq \"after_initialize\"\n        model.priority.should eq 1000\n      end\n\n      describe \"with default values\" do\n        it \"correctly applies values\" do\n          model = DefaultValues.from_yaml(%(---\\nname: Bob))\n          model.name.should eq \"Bob\"\n          model.age.should eq 0.0\n          model.is_alive.should be_true\n        end\n      end\n    end\n\n    describe \"#to_yaml\" do\n      it \"emits nil values when told\" do\n        t = TodoEmitNull.new(name: \"test todo\", priority: 20)\n        result = %(---\\nid:\\nname: test todo\\npriority: 20\\ncreated_at:\\nupdated_at:\\n)\n        t.to_yaml.should eq result\n      end\n\n      it \"does not emit nil values by default\" do\n        t = Todo.new(name: \"test todo\", priority: 20)\n        result = %(---\\nname: test todo\\npriority: 20\\n)\n\n        t.to_yaml.should eq result\n      end\n\n      it \"works with array of models\" do\n        todos = [\n          Todo.new(name: \"todo 1\", priority: 1),\n          Todo.new(name: \"todo 2\", priority: 2),\n          Todo.new(name: \"todo 3\", priority: 3),\n        ]\n\n        collection = todos.to_yaml\n        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)\n      end\n    end\n\n    context \"with yaml_options\" do\n      model = TodoYamlOptions.from_yaml(%(---\\ntask_name: The Task\\npriority: 9000))\n      it \"should deserialize correctly\" do\n        model.name.should eq \"The Task\"\n        model.priority.should be_nil\n      end\n\n      it \"should serialize correctly\" do\n        model.to_yaml.should eq %(---\\ntask_name: The Task\\n)\n      end\n\n      describe \"when using timestamp fields\" do\n        TodoYamlOptions.import([\n          TodoYamlOptions.new(name: \"first todo\", priority: 200),\n          TodoYamlOptions.new(name: \"second todo\", priority: 500),\n          TodoYamlOptions.new(name: \"third todo\", priority: 300),\n        ])\n\n        it \"should serialize correctly\" do\n          todos = TodoYamlOptions.order(id: :asc).select\n          todos[0].to_yaml.should eq %(---\\nid: 1\\ntask_name: first todo\\nposted: #{Time::Format::YAML_DATE.format(todos[0].created_at!)}\\n)\n          todos[1].to_yaml.should eq %(---\\nid: 2\\ntask_name: second todo\\nposted: #{Time::Format::YAML_DATE.format(todos[1].created_at!)}\\n)\n          todos[2].to_yaml.should eq %(---\\nid: 3\\ntask_name: third todo\\nposted: #{Time::Format::YAML_DATE.format(todos[2].created_at!)}\\n)\n        end\n      end\n    end\n  end\n\n  describe \"#to_h\" do\n    it \"convert object to hash\" do\n      t = Todo.new(name: \"test todo\", priority: 20)\n      result = {\"id\" => nil, \"name\" => \"test todo\", \"priority\" => 20, \"created_at\" => nil, \"updated_at\" => nil}\n\n      t.to_h.should eq result\n    end\n\n    it \"honors custom primary key\" do\n      s = Item.new(item_name: \"Hacker News\")\n      s.item_id = \"three\"\n      s.to_h.should eq({\"item_name\" => \"Hacker News\", \"item_id\" => \"three\"})\n    end\n\n    it \"works with enums\" do\n      model = EnumModel.new\n      model.my_enum = MyEnum::One\n      model.to_h.should eq({\"id\" => nil, \"my_enum\" => MyEnum::One})\n    end\n  end\n\n  # Only PG supports array types\n  {% if env(\"CURRENT_ADAPTER\") == \"pg\" %}\n    describe \"Array(T)\" do\n      describe \"with values\" do\n        it \"should instantiate correctly\" do\n          model = ArrayModel.new str_array: [\"foo\", \"bar\"]\n          model.str_array.should eq [\"foo\", \"bar\"]\n        end\n\n        it \"should save correctly\" do\n          model = ArrayModel.new\n          model.id = 1\n          model.str_array = [\"jack\", \"john\", \"jill\"]\n          model.i16_array = [10_000_i16, 20_000_i16, 30_000_i16]\n          model.i32_array = [1_000_000_i32, 2_000_000_i32, 3_000_000_i32, 4_000_000_i32]\n          model.i64_array = [100_000_000_000_i64, 200_000_000_000_i64, 300_000_000_000_i64, 400_000_000_000_i64]\n          model.f32_array = [1.123_456_78_f32, 1.234_567_899_998_741_4_f32]\n          model.f64_array = [1.123_456_789_011_23_f64, 1.234_567_899_998_741_4_f64]\n          model.bool_array = [true, true, false, true, false, false]\n          model.save.should be_true\n        end\n\n        it \"should read correctly\" do\n          model = ArrayModel.find! 1\n          model.str_array!.should be_a Array(String)\n          model.str_array!.should eq [\"jack\", \"john\", \"jill\"]\n          model.i16_array!.should be_a Array(Int16)\n          model.i16_array!.should eq [10_000_i16, 20_000_i16, 30_000_i16]\n          model.i32_array!.should be_a Array(Int32)\n          model.i32_array!.should eq [1_000_000_i32, 2_000_000_i32, 3_000_000_i32, 4_000_000_i32]\n          model.i64_array!.should be_a Array(Int64)\n          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]\n          model.f32_array!.should be_a Array(Float32)\n          model.f32_array!.should eq [1.123_456_78_f32, 1.234_567_899_998_741_4_f32]\n          model.f64_array!.should be_a Array(Float64)\n          model.f64_array!.should eq [1.123_456_789_011_23_f64, 1.234_567_899_998_741_4_f64]\n          model.bool_array!.should be_a Array(Bool)\n          model.bool_array!.should eq [true, true, false, true, false, false]\n        end\n      end\n\n      describe \"with empty array\" do\n        it \"should save correctly\" do\n          model = ArrayModel.new\n          model.id = 2\n          model.str_array = [] of String\n          model.f64_array.should be_a(Array(Float64))\n          model.f64_array.should eq [] of Float64\n          model.save.should be_true\n        end\n\n        it \"should read correctly\" do\n          model = ArrayModel.find! 2\n          model.str_array.should be_a Array(String)?\n          model.str_array!.should eq [] of String\n          model.i16_array.should be_nil\n          model.i32_array.should be_nil\n          model.i64_array.should be_nil\n          model.f32_array.should be_nil\n          model.f64_array.should be_a(Array(Float64))\n          model.f64_array.should eq [] of Float64\n          model.bool_array.should be_nil\n        end\n      end\n    end\n  {% end %}\nend\n"
  },
  {
    "path": "spec/mocks/db_mock.cr",
    "content": "class FakeStatement < DB::Statement\n  protected def perform_query(args : Enumerable) : DB::ResultSet\n    FieldEmitter.new\n  end\n\n  protected def perform_exec(args : Enumerable) : DB::ExecResult\n    DB::ExecResult.new 0_i64, 0_i64\n  end\nend\n\nclass FakeContext\n  include DB::ConnectionContext\n\n  def uri : URI\n    URI.new \"\"\n  end\n\n  def prepared_statements? : Bool\n    false\n  end\n\n  def discard(connection); end\n\n  def release(connection); end\nend\n\nclass FakeConnection < DB::Connection\n  def initialize\n    super(DB::Connection::Options.new)\n    @context = FakeContext.new\n    @prepared_statements = false\n  end\n\n  def build_unprepared_statement(query) : FakeStatement\n    FakeStatement.new self, query\n  end\n\n  def build_prepared_statement(query) : FakeStatement\n    FakeStatement.new self, query\n  end\nend\n\nalias EmitterType = DB::Any | PG::Numeric | JSON::Any | Int16\n\n# FieldEmitter emulates the subtle and uninformed way that\n# DB::ResultSet emits data. To be used in testing interactions\n# with raw data sets.\nclass FieldEmitter < DB::ResultSet\n  # 1. Override `#move_next` to move to the next row.\n  # 2. Override `#read` returning the next value in the row.\n  # 3. (Optional) Override `#read(t)` for some types `t` for which custom logic other than a simple cast is needed.\n  # 4. Override `#column_count`, `#column_name`.\n\n  @position = 0\n  @field_position = 0\n  @values = [] of EmitterType\n\n  def initialize\n    @statement = FakeStatement.new FakeConnection.new, \"\"\n  end\n\n  def _set_values(values : Array(EmitterType))\n    @values = [] of EmitterType\n    values.each do |v|\n      @values << v\n    end\n  end\n\n  def move_next : Bool\n    @position += 1\n    @field_position = 0\n    @position < @values.size\n  end\n\n  def read\n    if @position >= @values.size\n      raise \"Overread\"\n    end\n\n    @values[@position].tap do\n      @position += 1\n    end\n  end\n\n  def column_count : Int32\n    @values.size\n  end\n\n  def column_name(index : Int32) : String\n    \"Column #{index}\"\n  end\n\n  def next_column_index : Int32\n    @field_position\n  end\nend\n"
  },
  {
    "path": "spec/run_all_specs.sh",
    "content": "#! /bin/bash\n\nsource .env\n\necho \"Testing PG\"\n\ndocker-compose -f docker/docker-compose.pg.yml build spec\ndocker-compose -f docker/docker-compose.pg.yml run spec\n\necho \"Testing mysql\"\n\ndocker-compose -f docker/docker-compose.mysql.yml build spec\ndocker-compose -f docker/docker-compose.mysql.yml run spec\n\necho \"Testing sqlite\"\n\ndocker-compose -f docker/docker-compose.sqlite.yml build spec\ndocker-compose -f docker/docker-compose.sqlite.yml run spec\n\necho \"Done testing...stopping/removing images\"\n\ndocker-compose -f docker/docker-compose.sqlite.yml down\ndocker-compose -f docker/docker-compose.mysql.yml down\ndocker-compose -f docker/docker-compose.pg.yml down\n"
  },
  {
    "path": "spec/run_test_dbs.sh",
    "content": "#!/bin/bash\n\nMYSQL_VERSION=${MYSQL_VERSION:-5.7}\nPG_VERSION=${PG_VERSION:-15.2}\n\ndocker run --name mysql -d \\\n  -e MYSQL_ROOT_PASSWORD=password \\\n  -e MYSQL_DATABASE=granite_db \\\n  -e MYSQL_USER=granite \\\n  -e MYSQL_PASSWORD=password \\\n  -p 3306:3306 \\\n  mysql:${MYSQL_VERSION}\n\ndocker run --name psql -d \\\n  -e POSTGRES_USER=granite \\\n  -e POSTGRES_PASSWORD=password \\\n  -e POSTGRES_DB=granite_db \\\n  -p 5432:5432 \\\n  postgres:${PG_VERSION}\n"
  },
  {
    "path": "spec/spec_helper.cr",
    "content": "require \"mysql\"\nrequire \"pg\"\nrequire \"sqlite3\"\n\nCURRENT_ADAPTER     = ENV[\"CURRENT_ADAPTER\"]\nADAPTER_URL         = ENV[\"#{CURRENT_ADAPTER.upcase}_DATABASE_URL\"]\nADAPTER_REPLICA_URL = ENV[\"#{CURRENT_ADAPTER.upcase}_REPLICA_URL\"]? || ADAPTER_URL\n\ncase CURRENT_ADAPTER\nwhen \"pg\"\n  Granite::Connections << Granite::Adapter::Pg.new(name: CURRENT_ADAPTER, url: ADAPTER_URL)\n  Granite::Connections << {name: \"pg_with_replica\", writer: ADAPTER_URL, reader: ADAPTER_REPLICA_URL, adapter_type: Granite::Adapter::Pg}\nwhen \"mysql\"\n  Granite::Connections << Granite::Adapter::Mysql.new(name: CURRENT_ADAPTER, url: ADAPTER_URL)\n  Granite::Connections << {name: \"mysql_with_replica\", writer: ADAPTER_URL, reader: ADAPTER_REPLICA_URL, adapter_type: Granite::Adapter::Mysql}\nwhen \"sqlite\"\n  Granite::Connections << Granite::Adapter::Sqlite.new(name: CURRENT_ADAPTER, url: ADAPTER_URL)\n  Granite::Connections << {name: \"sqlite_with_replica\", writer: ADAPTER_URL, reader: ADAPTER_REPLICA_URL, adapter_type: Granite::Adapter::Sqlite}\nwhen Nil\n  raise \"Please set CURRENT_ADAPTER\"\nelse\n  raise \"Unknown adapter #{CURRENT_ADAPTER}\"\nend\n\nrequire \"spec\"\nrequire \"../src/granite\"\nrequire \"../src/adapter/**\"\nrequire \"./spec_models\"\nrequire \"./mocks/**\"\n\nSpec.before_suite do\n  Granite.settings.default_timezone = Granite::TIME_ZONE\n  {% if flag?(:spec_logs) %}\n    ::Log.builder.bind(\n      # source: \"spec.client\",\n      source: \"*\",\n      level: ::Log::Severity::Trace,\n      backend: ::Log::IOBackend.new(STDOUT, dispatcher: :sync),\n    )\n  {% end %}\nend\n\nSpec.before_each do\n  # I have no idea why this is needed, but it is.\n  Granite.settings.default_timezone = Granite::TIME_ZONE\nend\n\n{% if env(\"CURRENT_ADAPTER\") == \"mysql\" && !flag?(:issue_473) %}\n  Spec.after_each do\n    # https://github.com/amberframework/granite/issues/473\n    Granite::Connections[\"mysql\"].not_nil![:writer].try &.database.pool.close\n  end\n{% end %}\n"
  },
  {
    "path": "spec/spec_models.cr",
    "content": "require \"uuid\"\n\nclass Granite::Base\n  def self.drop_and_create\n  end\nend\n\n{% begin %}\n  {% adapter_literal = env(\"CURRENT_ADAPTER\").id %}\n\n  class ReplicatedChat < Granite::Base\n    connection {{ \"#{adapter_literal}_with_replica\" }}\n    table replicated_chats\n\n    column id : Int64, primary: true\n    column content : String\n  end\n\n  class Chat < Granite::Base\n    connection {{ adapter_literal }}\n    table chats\n\n    column id : Int64, primary: true\n\n    column name : String\n\n    has_one settings : ChatSettings, foreign_key: :chat_id\n  end\n\n  class ChatSettings < Granite::Base\n    connection {{ adapter_literal }}\n    table chat_settings\n\n    belongs_to chat : Chat, primary: true\n\n    column flood_limit : Int32\n  end\n\n  class Parent < Granite::Base\n    connection {{ adapter_literal }}\n    table parents\n\n    column id : Int64, primary: true\n    column name : String?\n    timestamps\n\n    has_many :students, class_name: Student\n\n    validate :name, \"Name cannot be blank\" do |parent|\n      !parent.name.to_s.blank?\n    end\n  end\n\n  class Teacher < Granite::Base\n    connection {{ adapter_literal }}\n    table teachers\n\n    column id : Int64, primary: true\n    column name : String?\n\n    has_many :klasses, class_name: Klass\n  end\n\n  class Student < Granite::Base\n    connection {{ adapter_literal }}\n    table students\n\n    column id : Int64, primary: true\n    column name : String?\n\n    has_many :enrollments, class_name: Enrollment\n    has_many :klasses, class_name: Klass, through: :enrollments\n  end\n\n  class Klass < Granite::Base\n    connection {{ adapter_literal }}\n    table klasses\n\n    column id : Int64, primary: true\n    column name : String?\n\n    belongs_to teacher : Teacher\n\n    has_many :enrollments, class_name: Enrollment\n    has_many :students, class_name: Student, through: :enrollments\n  end\n\n  class Enrollment < Granite::Base\n    connection {{ adapter_literal }}\n    table enrollments\n\n    column id : Int64, primary: true\n\n    belongs_to :student\n    belongs_to :klass\n  end\n\n  class School < Granite::Base\n    connection {{ adapter_literal }}\n    table schools\n\n    column custom_id : Int64, primary: true\n    column name : String?\n  end\n\n  class User < Granite::Base\n    connection {{ adapter_literal }}\n    table users\n\n    column id : Int64, primary: true\n    column email : String?\n\n    has_one :profile\n  end\n\n  class Character < Granite::Base\n    connection {{ adapter_literal }}\n    table characters\n\n    column character_id : Int32, primary: true\n    column name : String\n  end\n\n  class Courier < Granite::Base\n    connection {{ adapter_literal }}\n    table couriers\n\n    column courier_id : Int32, primary: true, auto: false\n    column issuer_id : Int32\n\n    belongs_to service : CourierService, primary_key: \"owner_id\"\n    has_one issuer : Character, primary_key: \"issuer_id\", foreign_key: \"character_id\"\n  end\n\n  class CourierService < Granite::Base\n    connection {{ adapter_literal }}\n    table services\n\n    column owner_id : Int64, primary: true, auto: false\n    column name : String\n\n    has_many :couriers, class_name: Courier, foreign_key: \"service_id\"\n  end\n\n  class Profile < Granite::Base\n    connection {{ adapter_literal }}\n    table profiles\n\n    column id : Int64, primary: true\n    column name : String?\n\n    belongs_to :user\n  end\n\n  class Nation::County < Granite::Base\n    connection {{ adapter_literal }}\n    table nation_counties\n\n    column id : Int64, primary: true\n    column name : String?\n  end\n\n  class Review < Granite::Base\n    connection {{ adapter_literal }}\n    table reviews\n\n    column id : Int64, primary: true\n    column name : String?\n    column downvotes : Int32?\n    column upvotes : Int64?\n    column sentiment : Float32?\n    column interest : Float64?\n    column published : Bool?\n    column created_at : Time?\n  end\n\n  class Empty < Granite::Base\n    connection {{ adapter_literal }}\n    table empties\n\n    column id : Int64, primary: true\n  end\n\n  class ReservedWord < Granite::Base\n    connection {{ adapter_literal }}\n    table \"select\"\n\n    column id : Int64, primary: true\n    column all : String?\n  end\n\n  class Callback < Granite::Base\n    connection {{ adapter_literal }}\n    table callbacks\n\n    column id : Int64, primary: true\n    column name : String?\n\n    property history : IO::Memory = IO::Memory.new\n\n    {% for name in Granite::Callbacks::CALLBACK_NAMES %}\n      {{name.id}} _{{name.id}}\n      private def _{{name.id}}\n        history << \"{{name.id}}\\n\"\n      end\n    {% end %}\n  end\n\n  class CallbackWithAbort < Granite::Base\n    connection {{ adapter_literal }}\n    table callbacks_with_abort\n\n    column abort_at : String, primary: true, auto: false\n    column do_abort : Bool?\n    column name : String?\n\n    property history : IO::Memory = IO::Memory.new\n\n    {% for name in Granite::Callbacks::CALLBACK_NAMES %}\n      {{name.id}} do\n        abort! if do_abort && abort_at == \"{{name.id}}\"\n        history << \"{{name.id}}\\n\"\n      end\n    {% end %}\n  end\n\n  class Kvs < Granite::Base\n    connection {{ adapter_literal }}\n    table kvs\n\n    column k : String, primary: true, auto: false\n    column v : String?\n  end\n\n  class Person < Granite::Base\n    connection {{ adapter_literal }}\n    table people\n\n    column id : Int64, primary: true\n    column name : String?\n  end\n\n  class Company < Granite::Base\n    connection {{ adapter_literal }}\n    table companies\n\n    column id : Int32, primary: true\n    column name : String?\n  end\n\n  class Book < Granite::Base\n    connection {{ adapter_literal }}\n    table books\n\n    column id : Int32, primary: true\n    column name : String?\n\n    @[JSON::Field(ignore: true)]\n    @[YAML::Field(ignore: true)]\n    belongs_to publisher : Company, foreign_key: publisher_id : Int32?\n    has_many :book_reviews, class_name: BookReview\n    belongs_to author : Person\n  end\n\n  class BookReview < Granite::Base\n    connection {{ adapter_literal }}\n    table book_reviews\n\n    column id : Int32, primary: true\n    column body : String?\n\n    belongs_to book : Book, foreign_key: book_id : Int32?\n  end\n\n  class Item < Granite::Base\n    connection {{ adapter_literal }}\n    table items\n\n    column item_id : String, primary: true, auto: false\n    column item_name : String?\n\n    before_create :generate_uuid\n\n    def generate_uuid\n      @item_id = UUID.random.to_s\n    end\n  end\n\n  class NonAutoDefaultPK < Granite::Base\n    connection {{ adapter_literal }}\n    table non_auto_default_pk\n\n    column id : Int64, primary: true, auto: false\n    column name : String?\n  end\n\n  class NonAutoCustomPK < Granite::Base\n    connection {{ adapter_literal }}\n    table non_auto_custom_pk\n\n    column custom_id : Int64, primary: true, auto: false\n    column name : String?\n  end\n\n  class Article < Granite::Base\n    connection {{ adapter_literal }}\n    table articles\n\n    column id : Int64, primary: true\n    column articlebody : String?\n  end\n\n  class Comment < Granite::Base\n    connection {{ adapter_literal }}\n    table comments\n\n    column id : Int64, primary: true\n    column commentbody : String?\n    column articleid : Int64?\n  end\n\n  class SongThread < Granite::Base\n    connection {{ env(\"CURRENT_ADAPTER\").id }}\n\n    column id : Int64, primary: true\n    column name : String?\n  end\n\n  class CustomSongThread < Granite::Base\n    connection {{ env(\"CURRENT_ADAPTER\").id }}\n    table custom_table_name\n\n    column custom_primary_key : Int64, primary: true\n    column name : String?\n  end\n\n  @[JSON::Serializable::Options(emit_nulls: true)]\n  @[YAML::Serializable::Options(emit_nulls: true)]\n  class TodoEmitNull < Granite::Base\n    connection {{ adapter_literal }}\n    table todos\n\n    column id : Int64, primary: true\n    column name : String?\n    column priority : Int32?\n    timestamps\n  end\n\n  class Todo < Granite::Base\n    connection {{ adapter_literal }}\n    table todos\n\n    column id : Int64, primary: true\n    column name : String?\n    column priority : Int32?\n    timestamps\n  end\n\n  class AfterInit < Granite::Base\n    connection {{ adapter_literal }}\n    table after_json_init\n\n    column id : Int64, primary: true\n    column name : String?\n    column priority : Int32?\n\n    def after_initialize\n      @priority = 1000\n    end\n  end\n\n  class ArticleViewModel < Granite::Base\n    connection {{ adapter_literal }}\n\n    column id : Int64, primary: true\n    column articlebody : String?\n    column commentbody : String?\n\n    select_statement <<-SQL\n      SELECT articles.id, articles.articlebody, comments.commentbody FROM articles JOIN comments ON comments.articleid = articles.id\n    SQL\n  end\n\n  # Only PG supports array types\n  {% if env(\"CURRENT_ADAPTER\") == \"pg\" %}\n    class ArrayModel < Granite::Base\n      connection {{ adapter_literal }}\n\n      column id : Int32, primary: true\n      column str_array : Array(String)?\n      column i16_array : Array(Int16)?\n      column i32_array : Array(Int32)?\n      column i64_array : Array(Int64)?\n      column f32_array : Array(Float32)?\n      column f64_array : Array(Float64)? = [] of Float64\n      column bool_array : Array(Bool)?\n    end\n    ArrayModel.migrator.drop_and_create\n  {% end %}\n\n  class UUIDModel < Granite::Base\n    connection {{ adapter_literal }}\n    table uuids\n\n    column uuid : UUID?, primary: true\n  end\n\n  class UUIDRelation < Granite::Base\n    connection {{ adapter_literal }}\n    table uuid_relations\n\n    column uuid : UUID?, primary: true, converter: Granite::Converters::Uuid(String)\n    belongs_to uuid_model : UUIDModel, foreign_key: uuid_model_id : UUID, primary_key: :uuid, converter: Granite::Converters::Uuid(String)\n  end\n\n  class UUIDNaturalModel < Granite::Base\n    connection {{ adapter_literal }}\n    table uuids\n\n    column uuid : UUID, primary: true, auto: false\n    column field_uuid : UUID?\n  end\n\n  class TodoJsonOptions < Granite::Base\n    connection {{ adapter_literal }}\n    table todos_json\n\n    column id : Int64, primary: true\n\n    @[JSON::Field(key: \"task_name\")]\n    column name : String?\n\n    @[JSON::Field(ignore: true)]\n    column priority : Int32?\n\n    @[JSON::Field(ignore: true)]\n    column updated_at : Time?\n\n    @[JSON::Field(key: \"posted\")]\n    column created_at : Time?\n  end\n\n  class TodoYamlOptions < Granite::Base\n    connection {{ adapter_literal }}\n    table todos_yaml\n\n    column id : Int64, primary: true\n\n    @[YAML::Field(key: \"task_name\")]\n    column name : String?\n\n    @[YAML::Field(ignore: true)]\n    column priority : Int32?\n\n    @[YAML::Field(ignore: true)]\n    column updated_at : Time?\n\n    @[YAML::Field(key: \"posted\")]\n    column created_at : Time?\n  end\n\n  class DefaultValues < Granite::Base\n    connection {{ adapter_literal }}\n    table defaults\n\n    column id : Int64, primary: true\n    column name : String = \"Jim\"\n    column is_alive : Bool = true\n    column age : Float64 = 0.0\n  end\n\n  class TimeTest < Granite::Base\n    connection {{ adapter_literal }}\n    table times\n\n    column id : Int64, primary: true\n    column test : Time?\n    column name : String?\n    timestamps\n  end\n\n  class ManualColumnType < Granite::Base\n    connection {{ adapter_literal }}\n    table manual_column_types\n\n    column id : Int64, primary: true\n    column foo : UUID?, column_type: \"DECIMAL(12, 10)\"\n  end\n\n  class EventCon < Granite::Base\n    connection {{ adapter_literal }}\n    table \"event_cons\"\n\n    column id : Int64, primary: true\n    column con_name : String\n    column event_name : String?\n\n    select_statement <<-SQL\n    select con_name FROM event_cons\n    SQL\n  end\n\n  class StringConversion < Granite::Base\n    connection {{ adapter_literal }}\n    table \"string_conversions\"\n\n    belongs_to :user\n\n    column id : Int64, primary: true\n    column int32 : Int32\n    column float32 : Float32\n    column float : Float64\n  end\n\n  class BoolModel < Granite::Base\n    connection {{ adapter_literal }}\n    table \"bool_model\"\n\n    column id : Int64, primary: true\n    column active : Bool = true\n  end\n\n  struct MyType\n    include JSON::Serializable\n\n    def initialize; end\n\n    property name : String = \"Jim\"\n    property age : Int32 = 12\n  end\n\n  enum MyEnum\n    Zero\n    One\n    Two\n    Three\n    Four\n  end\n\n  class EnumModel < Granite::Base\n    connection {{ adapter_literal }}\n    table enum_model\n\n    column id : Int64, primary: true\n    column my_enum : MyEnum?, column_type: \"TEXT\", converter: Granite::Converters::Enum(MyEnum, String)\n  end\n\n  class MyApp::Namespace::Model < Granite::Base\n    connection {{ adapter_literal }}\n\n    column id : Int64, primary: true\n  end\n\n  {% if env(\"CURRENT_ADAPTER\") == \"pg\" %}\n    class ConverterModel < Granite::Base\n      connection {{ adapter_literal }}\n      table converters\n\n      column id : Int64, primary: true\n\n      column binary_json : MyType?, column_type: \"BYTEA\", converter: Granite::Converters::Json(MyType, Bytes)\n      column string_json : MyType?, column_type: \"JSON\", converter: Granite::Converters::Json(MyType, JSON::Any)\n      column string_jsonb : MyType?, column_type: \"JSONB\", converter: Granite::Converters::Json(MyType, JSON::Any)\n\n      column smallint_enum : MyEnum?, column_type: \"SMALLINT\", converter: Granite::Converters::Enum(MyEnum, Int16)\n      column bigint_enum : MyEnum?, column_type: \"BIGINT\", converter: Granite::Converters::Enum(MyEnum, Int64)\n      column string_enum : MyEnum?, column_type: \"TEXT\", converter: Granite::Converters::Enum(MyEnum, String)\n      column enum_enum : MyEnum?, column_type: \"my_enum_type\", converter: Granite::Converters::Enum(MyEnum, Bytes)\n      column binary_enum : MyEnum?, column_type: \"BYTEA\", converter: Granite::Converters::Enum(MyEnum, Bytes)\n\n      column numeric : Float64?, column_type: \"DECIMAL(21, 20)\", converter: Granite::Converters::PgNumeric\n    end\n    ConverterModel.exec(<<-TYPE\n      DO $$\n      BEGIN\n          IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'my_enum_type') THEN\n            CREATE TYPE my_enum_type AS ENUM ('Zero', 'One', 'Two', 'Three', 'Four');\n          END IF;\n      END$$;\n      TYPE\n    )\n  {% elsif env(\"CURRENT_ADAPTER\") == \"sqlite\" %}\n    class ConverterModel < Granite::Base\n      connection {{ adapter_literal }}\n      table converters\n\n      column id : Int64, primary: true\n\n      column binary_json : MyType?, column_type: \"BLOB\", converter: Granite::Converters::Json(MyType, Bytes)\n      column string_json : MyType?, column_type: \"TEXT\", converter: Granite::Converters::Json(MyType, String)\n\n      column int_enum : MyEnum?, column_type: \"INTEGER\", converter: Granite::Converters::Enum(MyEnum, Int64)\n      column string_enum : MyEnum?, column_type: \"TEXT\", converter: Granite::Converters::Enum(MyEnum, String)\n      column binary_enum : MyEnum?, column_type: \"BLOB\", converter: Granite::Converters::Enum(MyEnum, String)\n    end\n  {% elsif env(\"CURRENT_ADAPTER\") == \"mysql\" %}\n    class ConverterModel < Granite::Base\n      connection {{ adapter_literal }}\n      table converters\n\n      column id : Int64, primary: true\n\n      column binary_json : MyType?, column_type: \"BLOB\", converter: Granite::Converters::Json(MyType, Bytes)\n      column string_json : MyType?, column_type: \"TEXT\", converter: Granite::Converters::Json(MyType, String)\n\n      column int_enum : MyEnum?, column_type: \"INTEGER\", converter: Granite::Converters::Enum(MyEnum, Int32)\n      column string_enum : MyEnum?, column_type: \"VARCHAR(5)\", converter: Granite::Converters::Enum(MyEnum, String)\n      column enum_enum : MyEnum?, column_type: \"ENUM('Zero', 'One', 'Two', 'Three', 'Four')\", converter: Granite::Converters::Enum(MyEnum, String)\n      column binary_enum : MyEnum?, column_type: \"BLOB\", converter: Granite::Converters::Enum(MyEnum, Bytes)\n    end\n  {% end %}\n\n  module Validators\n    class NilTest < Granite::Base\n      connection {{ adapter_literal }}\n\n      column id : Int64, primary: true\n\n      column first_name_not_nil : String?\n      column last_name_not_nil : String?\n      column age_not_nil : Int32?\n      column born_not_nil : Bool?\n      column value_not_nil : Float32?\n\n      column first_name : String?\n      column last_name : String?\n      column age : Int32?\n      column born : Bool?\n      column value : Float32?\n\n      validate_not_nil \"first_name_not_nil\"\n      validate_not_nil :last_name_not_nil\n      validate_not_nil :age_not_nil\n      validate_not_nil \"born_not_nil\"\n      validate_not_nil :value_not_nil\n\n      validate_is_nil \"first_name\"\n      validate_is_nil :last_name\n      validate_is_nil :age\n      validate_is_nil \"born\"\n      validate_is_nil :value\n    end\n\n    class BlankTest < Granite::Base\n      connection {{ adapter_literal }}\n\n      column id : Int64, primary: true\n\n      column first_name_not_blank : String?\n      column last_name_not_blank : String?\n\n      column first_name_is_blank : String?\n      column last_name_is_blank : String?\n\n      validate_not_blank \"first_name_not_blank\"\n      validate_not_blank \"last_name_not_blank\"\n\n      validate_is_blank \"first_name_is_blank\"\n      validate_is_blank \"last_name_is_blank\"\n    end\n\n    class ChoiceTest < Granite::Base\n      connection {{ adapter_literal }}\n\n      column id : Int64, primary: true\n\n      column number_symbol : Int32?\n      column type_array_symbol : String?\n\n      column number_string : Int32?\n      column type_array_string : String?\n\n      validate_is_valid_choice :number_symbol, [1, 2, 3]\n      validate_is_valid_choice :type_array_symbol, [:internal, :external, :third_party]\n      validate_is_valid_choice \"number_string\", [4, 5, 6]\n      validate_is_valid_choice \"type_array_string\", [\"internal\", \"external\", \"third_party\"]\n    end\n\n    class LessThanTest < Granite::Base\n      connection {{ adapter_literal }}\n\n      column id : Int64, primary: true\n\n      column int_32_lt : Int32?\n      column float_32_lt : Float32?\n\n      column int_32_lte : Int32?\n      column float_32_lte : Float32?\n\n      validate_less_than \"int_32_lt\", 10\n      validate_less_than :float_32_lt, 20.5\n\n      validate_less_than :int_32_lte, 50, true\n      validate_less_than \"float_32_lte\", 100.25, true\n    end\n\n    class GreaterThanTest < Granite::Base\n      connection {{ adapter_literal }}\n\n      column id : Int64, primary: true\n\n      column int_32_lt : Int32?\n      column float_32_lt : Float32?\n\n      column int_32_lte : Int32?\n      column float_32_lte : Float32?\n\n      validate_greater_than \"int_32_lt\", 10\n      validate_greater_than :float_32_lt, 20.5\n\n      validate_greater_than :int_32_lte, 50, true\n      validate_greater_than \"float_32_lte\", 100.25, true\n    end\n\n    class LengthTest < Granite::Base\n      connection {{ adapter_literal }}\n\n      column id : Int64, primary: true\n      column title : String?\n      column description : String?\n\n      validate_min_length :title, 5\n      validate_max_length :description, 25\n    end\n\n    class PersonUniqueness < Granite::Base\n      connection {{ adapter_literal }}\n\n      column id : Int64, primary: true\n      column name : String?\n\n      validate_uniqueness :name\n    end\n\n    class ExclusionTest < Granite::Base\n      connection {{ adapter_literal }}\n\n      column id : Int64, primary: true\n      column name : String?\n\n      validate_exclusion :name, [\"test_name\"]\n    end\n  end\n{% end %}\n\n{% for model in Granite::Base.all_subclasses %}\n  {{model.id}}.migrator.drop_and_create\n{% end %}\n"
  },
  {
    "path": "src/adapter/base.cr",
    "content": "require \"../granite\"\nrequire \"db\"\nrequire \"colorize\"\n\n# The Base Adapter specifies the interface that will be used by the model\n# objects to perform actions against a specific database.  Each adapter needs\n# to implement these methods.\nabstract class Granite::Adapter::Base\n  getter name : String\n  getter url : String\n  private property _database : DB::Database?\n\n  private SQL_KEYWORDS = Set(String).new(%w(\n    ALTER AND ANY AS ASC COLUMN CONSTRAINT COUNT CREATE DEFAULT DELETE DESC\n    DISTINCT DROP ELSE EXISTS FALSE FOREIGN FROM GROUP HAVING IF IN INDEX INNER\n    INSERT INTO JOIN LIMIT NOT NULL ON OR ORDER PRIMARY REFERENCES RELEASE RETURNING\n    SELECT SET TABLE THEN TRUE UNION UNIQUE UPDATE USING VALUES WHEN WHERE\n  ))\n\n  def initialize(@name : String, @url : String)\n  end\n\n  def database : DB::Database\n    @_database ||= DB.open(@url)\n  end\n\n  def open(&)\n    database.retry do\n      database.using_connection do |conn|\n        yield conn\n      rescue ex : IO::Error\n        raise ::DB::ConnectionLost.new(conn)\n      rescue ex : Exception\n        if ex.message =~ /client was disconnected/\n          raise ::DB::ConnectionLost.new(conn)\n        else\n          raise ex\n        end\n      end\n    end\n  end\n\n  def log(query : String, elapsed_time : Time::Span, params = [] of String) : Nil\n    Log.debug { colorize query, params, elapsed_time.total_seconds }\n  end\n\n  # remove all rows from a table and reset the counter on the id.\n  abstract def clear(table_name : String)\n\n  # select performs a query against a table.  The query object contains table_name,\n  # fields (configured using the sql_mapping directive in your model), and an optional\n  # raw query string.  The clause and params is the query and params that is passed\n  # in via .all() method\n  def select(query : Granite::Select::Container, clause = \"\", params = [] of DB::Any, &)\n    clause = ensure_clause_template(clause)\n    statement = query.custom ? \"#{query.custom} #{clause}\" : String.build do |stmt|\n      stmt << \"SELECT \"\n      stmt << query.fields.map { |name| \"#{quote(query.table_name)}.#{quote(name)}\" }.join(\", \")\n      stmt << \" FROM #{quote(query.table_name)} #{clause}\"\n    end\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.query statement, args: params do |rs|\n          yield rs\n        end\n      end\n    end\n\n    log statement, elapsed_time, params\n  end\n\n  # Returns `true` if a record exists that matches *criteria*, otherwise `false`.\n  def exists?(table_name : String, criteria : String, params = [] of Granite::Columns::Type) : Bool\n    statement = \"SELECT EXISTS(SELECT 1 FROM #{table_name} WHERE #{ensure_clause_template(criteria)})\"\n\n    exists = false\n    elapsed_time = Time.measure do\n      open do |db|\n        exists = db.query_one?(statement, args: params, as: Bool) || exists\n      end\n    end\n\n    log statement, elapsed_time, params\n\n    exists\n  end\n\n  protected def ensure_clause_template(clause : String) : String\n    clause\n  end\n\n  # This will insert a row in the database and return the id generated.\n  abstract def insert(table_name : String, fields, params, lastval) : Int64\n\n  # This will insert an array of models as one insert statement\n  abstract def import(table_name : String, primary_name : String, auto : Bool, fields, model_array, **options)\n\n  # This will update a row in the database.\n  abstract def update(table_name : String, primary_name : String, fields, params)\n\n  # This will delete a row from the database.\n  abstract def delete(table_name : String, primary_name : String, value)\n\n  module Schema\n    TYPES = {\n      \"Bool\"    => \"BOOL\",\n      \"Float32\" => \"FLOAT\",\n      \"Float64\" => \"REAL\",\n      \"Int32\"   => \"INT\",\n      \"Int64\"   => \"BIGINT\",\n      \"String\"  => \"VARCHAR(255)\",\n      \"Time\"    => \"TIMESTAMP\",\n    }\n  end\n\n  # Use macro in order to read a constant defined in each subclasses.\n  macro inherited\n    # quotes table and column names\n    def quote(name : String) : String\n      String.build do |str|\n        str << QUOTING_CHAR\n        str << name\n        str << QUOTING_CHAR\n      end\n    end\n\n    # converts the crystal class to database type of this adapter\n    def self.schema_type?(key : String) : String?\n      Schema::TYPES[key]? || Granite::Adapter::Base::Schema::TYPES[key]?\n    end\n  end\n\n  private def colorize(query : String, params, elapsed_time : Float64) : String\n    q = query.to_s.split(/([a-zA-Z0-9_$']+)/).map do |word|\n      if SQL_KEYWORDS.includes?(word.upcase)\n        word.colorize.bold.blue.to_s\n      elsif !word.starts_with?('$') && word =~ /\\d+/\n        word.colorize.light_red\n      elsif word.starts_with?('\\'') && word.ends_with?('\\'')\n        word.colorize(Colorize::Color256.new(193))\n      else\n        word.colorize.white\n      end\n    end.join\n\n    \"[#{humanize_duration(elapsed_time)}] #{q}: #{params.colorize.light_magenta}\"\n  end\n\n  private def humanize_duration(elapsed_time : Float64)\n    if elapsed_time > 0.1\n      \"#{(elapsed_time).*(100).trunc./(100)}s\".colorize.red\n    elsif elapsed_time > 0.001\n      \"#{(elapsed_time * 1_000).trunc}ms\".colorize.yellow\n    elsif elapsed_time > 0.000_001\n      \"#{(elapsed_time * 1_000_000).trunc}µs\".colorize.green\n    elsif elapsed_time > 0.000_000_001\n      \"#{(elapsed_time * 1_000_000_000).trunc}ns\".colorize.green\n    else\n      \"<1ns\".colorize.green\n    end\n  end\nend\n"
  },
  {
    "path": "src/adapter/mysql.cr",
    "content": "require \"./base\"\nrequire \"mysql\"\n\n# Mysql implementation of the Adapter\nclass Granite::Adapter::Mysql < Granite::Adapter::Base\n  QUOTING_CHAR = '`'\n\n  module Schema\n    TYPES = {\n      \"AUTO_Int32\" => \"INT NOT NULL AUTO_INCREMENT\",\n      \"AUTO_Int64\" => \"BIGINT NOT NULL AUTO_INCREMENT\",\n      \"AUTO_UUID\"  => \"CHAR(36)\",\n      \"Float64\"    => \"DOUBLE\",\n      \"UUID\"       => \"CHAR(36)\",\n      \"created_at\" => \"TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP\",\n      \"updated_at\" => \"TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP\",\n    }\n  end\n\n  # Using TRUNCATE instead of DELETE so the id column resets to 0\n  def clear(table_name : String)\n    statement = \"TRUNCATE #{quote(table_name)}\"\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement\n      end\n    end\n\n    log statement, elapsed_time\n  end\n\n  def insert(table_name : String, fields, params, lastval) : Int64\n    statement = String.build do |stmt|\n      stmt << \"INSERT INTO #{quote(table_name)} (\"\n      stmt << fields.map { |name| \"#{quote(name)}\" }.join(\", \")\n      stmt << \") VALUES (\"\n      stmt << fields.map { |_name| \"?\" }.join(\", \")\n      stmt << \")\"\n    end\n\n    last_id = -1_i64\n    elapsed_time = Time.measure do\n      open do |conn|\n        conn.exec statement, args: params\n        last_id = conn.scalar(last_val()).as(Int64) if lastval\n      end\n    end\n\n    log statement, elapsed_time, params\n\n    last_id\n  end\n\n  def import(table_name : String, primary_name : String, auto : Bool, fields, model_array, **options)\n    params = [] of Granite::Columns::Type\n\n    statement = String.build do |stmt|\n      stmt << \"INSERT\"\n      stmt << \" IGNORE\" if options[\"ignore_on_duplicate\"]?\n      stmt << \" INTO #{quote(table_name)} (\"\n      stmt << fields.map { |field| quote(field) }.join(\", \")\n      stmt << \") VALUES \"\n\n      model_array.each do |model|\n        model.set_timestamps\n        next unless model.valid?\n        stmt << \"(\"\n        stmt << Array.new(fields.size, '?').join(',')\n        params.concat fields.map { |field| model.read_attribute field }\n        stmt << \"),\"\n      end\n    end.chomp(',')\n\n    if options[\"update_on_duplicate\"]?\n      if columns = options[\"columns\"]?\n        statement += \" ON DUPLICATE KEY UPDATE \"\n        columns << \"updated_at\" if fields.includes? \"updated_at\"\n        columns.each do |key|\n          statement += \"#{quote(key)}=VALUES(#{quote(key)}), \"\n        end\n        statement = statement.chomp(\", \")\n      end\n    end\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement, args: params\n      end\n    end\n\n    log statement, elapsed_time, params\n  end\n\n  private def last_val : String\n    \"SELECT LAST_INSERT_ID()\"\n  end\n\n  # This will update a row in the database.\n  def update(table_name : String, primary_name : String, fields, params)\n    statement = String.build do |stmt|\n      stmt << \"UPDATE #{quote(table_name)} SET \"\n      stmt << fields.map { |name| \"#{quote(name)}=?\" }.join(\", \")\n      stmt << \" WHERE #{quote(primary_name)}=?\"\n    end\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement, args: params\n      end\n    end\n\n    log statement, elapsed_time, params\n  end\n\n  # This will delete a row from the database.\n  def delete(table_name : String, primary_name : String, value)\n    statement = \"DELETE FROM #{quote(table_name)} WHERE #{quote(primary_name)}=?\"\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement, value\n      end\n    end\n\n    log statement, elapsed_time, value\n  end\nend\n"
  },
  {
    "path": "src/adapter/pg.cr",
    "content": "require \"./base\"\nrequire \"pg\"\n\n# PostgreSQL implementation of the Adapter\nclass Granite::Adapter::Pg < Granite::Adapter::Base\n  QUOTING_CHAR = '\"'\n\n  module Schema\n    TYPES = {\n      \"Float32\"        => \"REAL\",\n      \"Float64\"        => \"DOUBLE PRECISION\",\n      \"String\"         => \"TEXT\",\n      \"AUTO_Int32\"     => \"SERIAL\",\n      \"AUTO_Int64\"     => \"BIGSERIAL\",\n      \"AUTO_UUID\"      => \"UUID\",\n      \"UUID\"           => \"UUID\",\n      \"created_at\"     => \"TIMESTAMP\",\n      \"updated_at\"     => \"TIMESTAMP\",\n      \"Array(String)\"  => \"TEXT[]\",\n      \"Array(Int16)\"   => \"SMALLINT[]\",\n      \"Array(Int32)\"   => \"INT[]\",\n      \"Array(Int64)\"   => \"BIGINT[]\",\n      \"Array(Float32)\" => \"REAL[]\",\n      \"Array(Float64)\" => \"DOUBLE PRECISION[]\",\n      \"Array(Bool)\"    => \"BOOLEAN[]\",\n    }\n  end\n\n  # remove all rows from a table and reset the counter on the id.\n  def clear(table_name : String)\n    statement = \"DELETE FROM #{quote(table_name)}\"\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement\n      end\n    end\n\n    log statement, elapsed_time\n  end\n\n  def insert(table_name : String, fields, params, lastval) : Int64\n    statement = String.build do |stmt|\n      stmt << \"INSERT INTO #{quote(table_name)} (\"\n      stmt << fields.map { |name| \"#{quote(name)}\" }.join(\", \")\n      stmt << \") VALUES (\"\n      stmt << position_str(fields.size)\n      stmt << \")\"\n\n      stmt << \" RETURNING #{quote(lastval)}\" if lastval\n    end\n\n    last_id = -1_i64\n    elapsed_time = Time.measure do\n      open do |db|\n        if lastval\n          last_id = db.scalar(statement, args: params).as(Int32 | Int64).to_i64\n        else\n          db.exec statement, args: params\n        end\n      end\n    end\n\n    log statement, elapsed_time, params\n\n    last_id\n  end\n\n  def import(table_name : String, primary_name : String, auto : Bool, fields, model_array, **options)\n    params = [] of Granite::Columns::Type\n    # PG fails when inserting null into AUTO INCREMENT PK field.\n    # If AUTO INCREMENT is TRUE AND all model's pk are nil, remove PK from fields list for AUTO INCREMENT to work properly\n    fields.reject! { |field| field == primary_name } if model_array.all? { |m| m.to_h[primary_name].nil? } && auto\n    index = 0\n\n    statement = String.build do |stmt|\n      stmt << \"INSERT\"\n      stmt << \" INTO #{quote(table_name)} (\"\n      stmt << fields.map { |field| quote(field) }.join(\", \")\n      stmt << \") VALUES \"\n\n      model_array.each do |model|\n        model.set_timestamps\n        next unless model.valid?\n        stmt << '('\n        stmt << fields.map_with_index { |_f, idx| \"$#{index + idx + 1}\" }.join(',')\n        params.concat fields.map { |field| model.read_attribute field }\n        stmt << \"),\"\n        index += fields.size\n      end\n    end.chomp(',')\n\n    if options[\"update_on_duplicate\"]?\n      if columns = options[\"columns\"]?\n        statement += \" ON CONFLICT (#{quote(primary_name)}) DO UPDATE SET \"\n        columns << \"updated_at\" if fields.includes? \"updated_at\"\n        columns.each do |key|\n          statement += \"#{quote(key)}=EXCLUDED.#{quote(key)}, \"\n        end\n      end\n      statement = statement.chomp(\", \")\n    elsif options[\"ignore_on_duplicate\"]?\n      statement += \" ON CONFLICT DO NOTHING\"\n    end\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement, args: params\n      end\n    end\n\n    log statement, elapsed_time, params\n  end\n\n  # This will update a row in the database.\n  def update(table_name : String, primary_name : String, fields, params)\n    statement = String.build do |stmt|\n      stmt << \"UPDATE #{quote(table_name)} SET \"\n      stmt << fields.map_with_index { |name, i| \"#{quote(name)}=$#{i + 1}\" }.join(\", \")\n      stmt << \" WHERE #{quote(primary_name)}=$#{fields.size + 1}\"\n    end\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement, args: params\n      end\n    end\n\n    log statement, elapsed_time, params\n  end\n\n  # This will delete a row from the database.\n  def delete(table_name : String, primary_name : String, value)\n    statement = \"DELETE FROM #{quote(table_name)} WHERE #{quote(primary_name)}=$1\"\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement, value\n      end\n    end\n\n    log statement, elapsed_time, value\n  end\n\n  protected def ensure_clause_template(clause : String) : String\n    if clause.includes?(\"?\")\n      num_subs = clause.count(\"?\")\n\n      num_subs.times do |i|\n        clause = clause.sub(\"?\", \"$#{i + 1}\")\n      end\n    end\n\n    clause\n  end\n\n  private def position_str(n : Int32) : String\n    i = 1\n    String.build do |str|\n      while i <= n\n        str << \"$\" << i\n        i += 1\n        str << \", \" if i <= n\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "src/adapter/sqlite.cr",
    "content": "require \"./base\"\nrequire \"sqlite3\"\n\n# Sqlite implementation of the Adapter\nclass Granite::Adapter::Sqlite < Granite::Adapter::Base\n  QUOTING_CHAR = '\"'\n\n  module Schema\n    TYPES = {\n      \"AUTO_Int32\" => \"INTEGER NOT NULL\",\n      \"AUTO_Int64\" => \"INTEGER NOT NULL\",\n      \"AUTO_UUID\"  => \"CHAR(36)\",\n      \"UUID\"       => \"CHAR(36)\",\n      \"Int32\"      => \"INTEGER\",\n      \"Int64\"      => \"INTEGER\",\n      \"created_at\" => \"VARCHAR\",\n      \"updated_at\" => \"VARCHAR\",\n    }\n  end\n\n  # remove all rows from a table and reset the counter on the id.\n  def clear(table_name : String)\n    statement = \"DELETE FROM #{quote(table_name)}\"\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement\n      end\n    end\n\n    log statement, elapsed_time\n  end\n\n  def insert(table_name : String, fields, params, lastval) : Int64\n    statement = String.build do |stmt|\n      stmt << \"INSERT INTO #{quote(table_name)} (\"\n      stmt << fields.map { |name| \"#{quote(name)}\" }.join(\", \")\n      stmt << \") VALUES (\"\n      stmt << fields.map { |_name| \"?\" }.join(\", \")\n      stmt << \")\"\n    end\n\n    last_id = -1_i64\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement, args: params\n        last_id = db.scalar(last_val()).as(Int64) if lastval\n      end\n    end\n\n    log statement, elapsed_time, params\n\n    last_id\n  end\n\n  def import(table_name : String, primary_name : String, auto : Bool, fields, model_array, **options)\n    params = [] of Granite::Columns::Type\n\n    statement = String.build do |stmt|\n      stmt << \"INSERT \"\n      if options[\"update_on_duplicate\"]?\n        stmt << \"OR REPLACE \"\n      elsif options[\"ignore_on_duplicate\"]?\n        stmt << \"OR IGNORE \"\n      end\n      stmt << \"INTO #{quote(table_name)} (\"\n      stmt << fields.map { |field| quote(field) }.join(\", \")\n      stmt << \") VALUES \"\n\n      model_array.each do |model|\n        next unless model.valid?\n        model.set_timestamps\n        stmt << '('\n        stmt << Array.new(fields.size, '?').join(',')\n        params.concat fields.map { |field| model.read_attribute field }\n        stmt << \"),\"\n      end\n    end.chomp(',')\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement, args: params\n      end\n    end\n\n    log statement, elapsed_time, params\n  end\n\n  private def last_val\n    \"SELECT LAST_INSERT_ROWID()\"\n  end\n\n  # This will update a row in the database.\n  def update(table_name : String, primary_name : String, fields, params)\n    statement = String.build do |stmt|\n      stmt << \"UPDATE #{quote(table_name)} SET \"\n      stmt << fields.map { |name| \"#{quote(name)}=?\" }.join(\", \")\n      stmt << \" WHERE #{quote(primary_name)}=?\"\n    end\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement, args: params\n      end\n    end\n\n    log statement, elapsed_time, params\n  end\n\n  # This will delete a row from the database.\n  def delete(table_name : String, primary_name : String, value)\n    statement = \"DELETE FROM #{quote(table_name)} WHERE #{quote(primary_name)}=?\"\n\n    elapsed_time = Time.measure do\n      open do |db|\n        db.exec statement, value\n      end\n    end\n\n    log statement, elapsed_time, value\n  end\nend\n"
  },
  {
    "path": "src/granite/association_collection.cr",
    "content": "class Granite::AssociationCollection(Owner, Target)\n  forward_missing_to all\n\n  def initialize(@owner : Owner, @foreign_key : (Symbol | String), @through : (Symbol | String | Nil) = nil, @primary_key : (Symbol | String | Nil) = nil)\n  end\n\n  def all(clause = \"\", params = [] of DB::Any)\n    Target.all(\n      [query, clause].join(\" \"),\n      [owner.primary_key_value] + params\n    )\n  end\n\n  def find_by(**args)\n    Target.first(\n      \"#{query} AND #{args.map { |arg| \"#{Target.quote(Target.table_name)}.#{Target.quote(arg.to_s)} = ?\" }.join(\" AND \")}\",\n      [owner.primary_key_value] + args.values.to_a\n    )\n  end\n\n  def find_by!(**args)\n    find_by(**args) || raise Granite::Querying::NotFound.new(\"No #{Target.name} found where #{args.map { |k, v| \"#{k} = #{v}\" }.join(\" and \")}\")\n  end\n\n  def find(value)\n    Target.find(value)\n  end\n\n  def find!(value)\n    Target.find!(value)\n  end\n\n  private getter owner\n  private getter foreign_key\n  private getter through\n\n  private def query\n    if through.nil?\n      \"WHERE #{Target.table_name}.#{@foreign_key} = ?\"\n    else\n      key = @primary_key || \"#{Target.to_s.underscore}_id\"\n      \"JOIN #{through} ON #{through}.#{key} = #{Target.table_name}.#{Target.primary_name} \" \\\n      \"WHERE #{through}.#{@foreign_key} = ?\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/associations.cr",
    "content": "module Granite::Associations\n  macro belongs_to(model, **options)\n    {% if model.is_a? TypeDeclaration %}\n      {% method_name = model.var %}\n      {% class_name = model.type %}\n    {% else %}\n      {% method_name = model.id %}\n      {% class_name = options[:class_name] || model.id.camelcase %}\n    {% end %}\n\n    {% if options[:foreign_key] && options[:foreign_key].is_a? TypeDeclaration %}\n      {% foreign_key = options[:foreign_key].var %}\n      column {{options[:foreign_key]}}{% if options[:primary] %}, primary: {{options[:primary]}}{% end %}{% if options[:converter] %}, converter: {{options[:converter]}}{% end %}\n    {% else %}\n      {% foreign_key = method_name + \"_id\" %}\n      column {{foreign_key}} : Int64?{% if options[:primary] %}, primary: {{options[:primary]}}{% end %}{% if options[:converter] %}, converter: {{options[:converter]}}{% end %}\n    {% end %}\n    {% primary_key = options[:primary_key] || \"id\" %}\n\n    @[Granite::Relationship(target: {{class_name.id}}, type: :belongs_to,\n      primary_key: {{primary_key.id}}, foreign_key: {{foreign_key.id}})]\n    def {{method_name.id}} : {{class_name.id}}?\n      if parent = {{class_name.id}}.find_by({{primary_key.id}}: {{foreign_key.id}})\n        parent\n      else\n        {{class_name.id}}.new\n      end\n    end\n\n    def {{method_name.id}}! : {{class_name.id}}\n      {{class_name.id}}.find_by!({{primary_key.id}}: {{foreign_key.id}})\n    end\n\n    def {{method_name.id}}=(parent : {{class_name.id}})\n      @{{foreign_key.id}} = parent.{{primary_key.id}}\n    end\n  end\n\n  macro has_one(model, **options)\n    {% if model.is_a? TypeDeclaration %}\n      {% method_name = model.var %}\n      {% class_name = model.type %}\n    {% else %}\n      {% method_name = model.id %}\n      {% class_name = options[:class_name] || model.id.camelcase %}\n    {% end %}\n    {% foreign_key = options[:foreign_key] || @type.stringify.split(\"::\").last.underscore + \"_id\" %}\n\n    {% if options[:primary_key] && options[:primary_key].is_a? TypeDeclaration %}\n      {% primary_key = options[:primary_key].var %}\n      column {{options[:primary_key]}}\n    {% else %}\n      {% primary_key = options[:primary_key] || \"id\" %}\n    {% end %}\n\n    @[Granite::Relationship(target: {{class_name.id}}, type: :has_one,\n      primary_key: {{primary_key.id}}, foreign_key: {{foreign_key.id}})]\n\n    def {{method_name}} : {{class_name}}?\n      {{class_name.id}}.find_by({{foreign_key.id}}: self.{{primary_key.id}})\n    end\n\n    def {{method_name}}! : {{class_name}}\n      {{class_name.id}}.find_by!({{foreign_key.id}}: self.{{primary_key.id}})\n    end\n\n    def {{method_name}}=(child)\n      child.{{foreign_key.id}} = self.{{primary_key.id}}\n    end\n  end\n\n  macro has_many(model, **options)\n    {% if model.is_a? TypeDeclaration %}\n      {% method_name = model.var %}\n      {% class_name = model.type %}\n    {% else %}\n      {% method_name = model.id %}\n      {% class_name = options[:class_name] || model.id.camelcase %}\n    {% end %}\n    {% foreign_key = options[:foreign_key] || @type.stringify.split(\"::\").last.underscore + \"_id\" %}\n    {% primary_key = options[:primary_key] || class_name.stringify.split(\"::\").last.underscore + \"_id\" %}\n    {% through = options[:through] %}\n    @[Granite::Relationship(target: {{class_name.id}}, through: {{through.id}}, type: :has_many,\n      primary_key: {{through}}, foreign_key: {{foreign_key.id}})]\n    def {{method_name.id}}\n      Granite::AssociationCollection(self, {{class_name.id}}).new(self, {{foreign_key}}, {{through}}, {{primary_key}})\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/base.cr",
    "content": "require \"./collection\"\nrequire \"./association_collection\"\nrequire \"./associations\"\nrequire \"./callbacks\"\nrequire \"./columns\"\nrequire \"./query/executors/base\"\nrequire \"./query/**\"\nrequire \"./settings\"\nrequire \"./table\"\nrequire \"./validators\"\nrequire \"./validation_helpers/**\"\nrequire \"./migrator\"\nrequire \"./select\"\nrequire \"./version\"\nrequire \"./connections\"\nrequire \"./integrators\"\nrequire \"./converters\"\nrequire \"./type\"\nrequire \"./connection_management\"\n\n# Granite::Base is the base class for your model objects.\nabstract class Granite::Base\n  include Associations\n  include Callbacks\n  include Columns\n  include Tables\n  include Transactions\n  include Validators\n  include ValidationHelpers\n  include Migrator\n  include Select\n  include Querying\n\n  include ConnectionManagement\n\n  extend Columns::ClassMethods\n  extend Tables::ClassMethods\n  extend Granite::Migrator::ClassMethods\n\n  extend Querying::ClassMethods\n  extend Query::BuilderMethods\n  extend Transactions::ClassMethods\n  extend Integrators\n  extend Select\n\n  macro inherited\n    protected class_getter select_container : Container = Container.new(table_name: table_name, fields: fields)\n\n    include JSON::Serializable\n    include YAML::Serializable\n\n    # Returns true if this object hasn't been saved yet.\n    @[JSON::Field(ignore: true)]\n    @[YAML::Field(ignore: true)]\n    disable_granite_docs? property? new_record : Bool = true\n\n    # Returns true if this object has been destroyed.\n    @[JSON::Field(ignore: true)]\n    @[YAML::Field(ignore: true)]\n    disable_granite_docs? getter? destroyed : Bool = false\n\n    # Returns true if the record is persisted.\n    disable_granite_docs? def persisted?\n      !(new_record? || destroyed?)\n    end\n\n    disable_granite_docs? def initialize(**args : Granite::Columns::Type)\n      set_attributes(args.to_h.transform_keys(&.to_s))\n    end\n\n    disable_granite_docs? def initialize(args : Granite::ModelArgs)\n      set_attributes(args.transform_keys(&.to_s))\n    end\n\n    disable_granite_docs? def initialize\n    end\n\n    before_save :switch_to_writer_adapter\n    before_destroy :switch_to_writer_adapter\n    after_save :update_last_write_time\n    after_save :schedule_adapter_switch\n    after_destroy :update_last_write_time\n    after_destroy :schedule_adapter_switch\n  end\nend\n"
  },
  {
    "path": "src/granite/callbacks.cr",
    "content": "module Granite::Callbacks\n  class Abort < Exception\n  end\n\n  CALLBACK_NAMES = %w(before_save after_save before_create after_create before_update after_update before_destroy after_destroy)\n\n  @[JSON::Field(ignore: true)]\n  @[YAML::Field(ignore: true)]\n  @_current_callback : String?\n\n  macro included\n    macro inherited\n      disable_granite_docs? CALLBACKS = {\n        {% for name in CALLBACK_NAMES %}\n          {{name.id}}: [] of Nil,\n        {% end %}\n      }\n      {% for name in CALLBACK_NAMES %}\n        disable_granite_docs? def {{name.id}}\n          __{{name.id}}\n        end\n      {% end %}\n    end\n  end\n\n  {% for name in CALLBACK_NAMES %}\n    macro {{name.id}}(*callbacks, &block)\n      \\{% for callback in callbacks %}\n        \\{% CALLBACKS[{{name}}] << callback %}\n      \\{% end %}\n      \\{% if block.is_a? Block %}\n        \\{% CALLBACKS[{{name}}] << block %}\n      \\{% end %}\n    end\n\n    macro __{{name.id}}\n      @_current_callback = {{name}}\n      \\{% for callback in CALLBACKS[{{name}}] %}\n        \\{% if callback.is_a? Block %}\n           begin\n             \\{{callback.body}}\n           end\n        \\{% else %}\n          \\{{callback.id}}\n        \\{% end %}\n      \\{% end %}\n    end\n  {% end %}\n\n  def abort!(message = \"Aborted at #{@_current_callback}.\")\n    raise Abort.new(message)\n  end\nend\n"
  },
  {
    "path": "src/granite/collection.cr",
    "content": "class Granite::Collection(M)\n  forward_missing_to collection\n\n  def initialize(@loader : -> Array(M))\n    @loaded = false\n    @collection = [] of M\n  end\n\n  def loaded?\n    @loaded\n  end\n\n  private getter loader\n\n  private def collection\n    return @collection if loaded?\n\n    @collection = loader.call\n    @loaded = true\n    @collection\n  end\nend\n"
  },
  {
    "path": "src/granite/columns.cr",
    "content": "require \"json\"\nrequire \"uuid\"\n\nmodule Granite::Columns\n  alias SupportedArrayTypes = Array(String) | Array(Int16) | Array(Int32) | Array(Int64) | Array(Float32) | Array(Float64) | Array(Bool) | Array(UUID)\n  alias Type = DB::Any | SupportedArrayTypes | UUID\n\n  module ClassMethods\n    # All fields\n    def fields : Array(String)\n      {% begin %}\n        {% columns = @type.instance_vars.select(&.annotation(Granite::Column)).map(&.name.stringify) %}\n        {{columns.empty? ? \"[] of String\".id : columns}}\n      {% end %}\n    end\n\n    # Columns minus the PK\n    def content_fields : Array(String)\n      {% begin %}\n        {% columns = @type.instance_vars.select { |ivar| (ann = ivar.annotation(Granite::Column)) && !ann[:primary] }.map(&.name.stringify) %}\n        {{columns.empty? ? \"[] of String\".id : columns}}\n      {% end %}\n    end\n  end\n\n  def content_values : Array(Granite::Columns::Type)\n    parsed_params = [] of Type\n    {% for column in @type.instance_vars.select { |ivar| (ann = ivar.annotation(Granite::Column)) && !ann[:primary] } %}\n      {% ann = column.annotation(Granite::Column) %}\n      parsed_params << {% if ann[:converter] %} {{ann[:converter]}}.to_db {{column.name.id}} {% else %} {{column.name.id}} {% end %}\n    {% end %}\n    parsed_params\n  end\n\n  # Consumes the result set to set self's property values.\n  def from_rs(result : DB::ResultSet) : Nil\n    {% begin %}\n      result.column_names.each do |col|\n        case col\n        {% for column in @type.instance_vars.select(&.annotation(Granite::Column)) %}\n          {% ann = column.annotation(Granite::Column) %}\n          when {{column.name.stringify}}\n            @{{column.id}} = {% if ann[:converter] %}\n              {{ann[:converter]}}.from_rs result\n            {% else %}\n              value = Granite::Type.from_rs(result, {{ann[:nilable] ? column.type : column.type.union_types.reject { |t| t == Nil }.first}})\n\n              {% if column.has_default_value? && !column.default_value.nil? %}\n                return {{column.default_value}} if value.nil?\n              {% end %}\n\n              value\n            {% end %}\n        {% end %}\n        else\n          # Skip\n        end\n      end\n    {% end %}\n  end\n\n  # Defines a column *decl* with the given *options*.\n  macro column(decl, **options)\n    {% type = decl.type %}\n    {% not_nilable_type = type.is_a?(Path) ? type.resolve : (type.is_a?(Union) ? type.types.reject(&.resolve.nilable?).first : (type.is_a?(Generic) ? type.resolve : type)) %}\n\n    # Raise an exception if the delc type has more than 2 union types or if it has 2 types without nil\n    # This prevents having a column typed to String | Int32 etc.\n    {% if type.is_a?(Union) && (type.types.size > 2 || (type.types.size == 2 && !type.types.any?(&.resolve.nilable?))) %}\n      {% raise \"The column #{@type.name}##{decl.var} cannot consist of a Union with a type other than `Nil`.\" %}\n    {% end %}\n\n    {% column_type = (options[:column_type] && !options[:column_type].nil?) ? options[:column_type] : nil %}\n    {% converter = (options[:converter] && !options[:converter].nil?) ? options[:converter] : nil %}\n    {% primary = (options[:primary] && !options[:primary].nil?) ? options[:primary] : false %}\n    {% auto = (options[:auto] && !options[:auto].nil?) ? options[:auto] : false %}\n    {% auto = (!options || (options && options[:auto] == nil)) && primary %}\n\n    {% nilable = (type.is_a?(Path) ? type.resolve.nilable? : (type.is_a?(Union) ? type.types.any?(&.resolve.nilable?) : (type.is_a?(Generic) ? type.resolve.nilable? : type.nilable?))) %}\n\n    @[Granite::Column(column_type: {{column_type}}, converter: {{converter}}, auto: {{auto}}, primary: {{primary}}, nilable: {{nilable}})]\n    @{{decl.var}} : {{decl.type}}? {% unless decl.value.is_a? Nop %} = {{decl.value}} {% end %}\n\n    {% if nilable || primary %}\n      def {{decl.var.id}}=(@{{decl.var.id}} : {{not_nilable_type}}?); end\n\n      def {{decl.var.id}} : {{not_nilable_type}}?\n        @{{decl.var}}\n      end\n\n      def {{decl.var.id}}! : {{not_nilable_type}}\n        raise NilAssertionError.new {{@type.name.stringify}} + \"#\" + {{decl.var.stringify}} + \" cannot be nil\" if @{{decl.var}}.nil?\n        @{{decl.var}}.not_nil!\n      end\n    {% else %}\n      def {{decl.var.id}}=(@{{decl.var.id}} : {{type.id}}); end\n\n      def {{decl.var.id}} : {{type.id}}\n        raise NilAssertionError.new {{@type.name.stringify}} + \"#\" + {{decl.var.stringify}} + \" cannot be nil\" if @{{decl.var}}.nil?\n        @{{decl.var}}.not_nil!\n      end\n    {% end %}\n  end\n\n  # include created_at and updated_at that will automatically be updated\n  macro timestamps\n    column created_at : Time?\n    column updated_at : Time?\n  end\n\n  def to_h\n    fields = {{\"Hash(String, Union(#{@type.instance_vars.select(&.annotation(Granite::Column)).map(&.type.id).splat})).new\".id}}\n\n    {% for column in @type.instance_vars.select(&.annotation(Granite::Column)) %}\n      {% nilable = (column.type.is_a?(Path) ? column.type.resolve.nilable? : (column.type.is_a?(Union) ? column.type.types.any?(&.resolve.nilable?) : (column.type.is_a?(Generic) ? column.type.resolve.nilable? : column.type.nilable?))) %}\n\n      begin\n      {% if column.type.id == Time.id %}\n        fields[\"{{column.name}}\"] = {{column.name.id}}.try(&.in(Granite.settings.default_timezone).to_s(Granite::DATETIME_FORMAT))\n      {% elsif column.type.id == Slice.id %}\n        fields[\"{{column.name}}\"] = {{column.name.id}}.try(&.to_s(\"\"))\n      {% else %}\n        fields[\"{{column.name}}\"] = {{column.name.id}}\n      {% end %}\n      rescue ex : NilAssertionError\n        {% if nilable %}\n        fields[\"{{column.name}}\"] = nil\n        {% end %}\n      end\n    {% end %}\n\n    fields\n  end\n\n  def set_attributes(hash : Hash(String | Symbol, T)) : self forall T\n    {% for column in @type.instance_vars.select { |ivar| (ann = ivar.annotation(Granite::Column)) && (!ann[:primary] || (ann[:primary] && ann[:auto] == false)) } %}\n      if hash.has_key?({{column.stringify}})\n        begin\n          val = Granite::Type.convert_type hash[{{column.stringify}}], {{column.type}}\n        rescue ex : ArgumentError\n          error =  Granite::ConversionError.new({{column.name.stringify}}, ex.message)\n        end\n\n        if !val.is_a? {{column.type}}\n          error = Granite::ConversionError.new({{column.name.stringify}}, \"Expected {{column.id}} to be {{column.type}} but got #{typeof(val)}.\")\n        else\n          @{{column}} = val\n        end\n\n        errors << error if error\n      end\n    {% end %}\n    self\n  end\n\n  def read_attribute(attribute_name : Symbol | String) : Type\n    {% begin %}\n      case attribute_name.to_s\n      {% for column in @type.instance_vars.select(&.annotation(Granite::Column)) %}\n        {% ann = column.annotation(Granite::Column) %}\n      when \"{{ column.name }}\"\n        {% if ann[:converter] %}\n          {{ann[:converter]}}.to_db @{{column.name.id}}\n        {% else %}\n          @{{ column.name.id }}\n        {% end %}\n      {% end %}\n      else\n        raise \"Cannot read attribute #{attribute_name}, invalid attribute\"\n      end\n    {% end %}\n  end\n\n  def primary_key_value\n    {% begin %}\n      {% primary_key = @type.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n      {% raise raise \"A primary key must be defined for #{@type.name}.\" unless primary_key %}\n      {{primary_key.id}}\n    {% end %}\n  end\nend\n"
  },
  {
    "path": "src/granite/connection_management.cr",
    "content": "module Granite::ConnectionManagement\n  macro included\n    # Default value for the time a model waits before using a reader\n    # database connection for read operations\n    # all models use this value. Change it\n    # to change it in all Granite::Base models.\n    class_property connection_switch_wait_period : Int32 = Granite::Connections.connection_switch_wait_period\n    @@last_write_time = Time.monotonic\n\n    class_property current_adapter : Granite::Adapter::Base?\n    class_property reader_adapter : Granite::Adapter::Base?\n    class_property writer_adapter : Granite::Adapter::Base?\n\n    def self.last_write_time\n      @@last_write_time\n    end\n\n    # This is done this way because callbacks don't work on class mthods\n    def self.update_last_write_time\n      @@last_write_time = Time.monotonic\n    end\n\n    def update_last_write_time\n      self.class.update_last_write_time\n    end\n\n    def self.time_since_last_write\n      Time.monotonic - @@last_write_time\n    end\n\n    def time_since_last_write\n      self.class.time_since_last_write\n    end\n\n    def self.switch_to_reader_adapter\n      if time_since_last_write > @@connection_switch_wait_period.milliseconds\n        @@current_adapter = @@reader_adapter\n      end\n    end\n\n    def switch_to_reader_adapter\n      self.class.switch_to_reader_adapter\n    end\n\n    def self.switch_to_writer_adapter\n      @@current_adapter = @@writer_adapter\n    end\n\n    def switch_to_writer_adapter\n      self.class.switch_to_writer_adapter\n    end\n\n    def self.schedule_adapter_switch\n      return if @@writer_adapter == @@reader_adapter\n\n      spawn do\n        sleep @@connection_switch_wait_period.milliseconds\n        switch_to_reader_adapter\n      end\n\n      Fiber.yield\n    end\n\n    def schedule_adapter_switch\n      self.class.schedule_adapter_switch\n    end\n\n    def self.adapter\n      begin\n        @@current_adapter.not_nil!\n      rescue NilAssertionError\n        Granite::Connections.registered_connections.first?.not_nil![:writer]\n      end\n    end\n  end\n\n  macro connection(name)\n    {% name = name.id.stringify %}\n\n    error_message = \"Connection #{{{name}}} not found in Granite::Connections.\n    Available connections are:\n\n    #{Granite::Connections.registered_connections.map{ |conn| \"#{conn[:writer].name}\"}.join(\", \")}\"\n\n    raise error_message if Granite::Connections[{{name}}].nil?\n\n    self.writer_adapter = Granite::Connections[{{name}}].not_nil![:writer]\n    self.reader_adapter = Granite::Connections[{{name}}].not_nil![:reader]\n    self.current_adapter = @@writer_adapter\n  end\nend\n"
  },
  {
    "path": "src/granite/connections.cr",
    "content": "module Granite\n  class Connections\n    class_property connection_switch_wait_period : Int32 = 2000\n    class_getter registered_connections = [] of {writer: Granite::Adapter::Base, reader: Granite::Adapter::Base}\n\n    # Registers the given *adapter*.  Raises if an adapter with the same name has already been registered.\n    def self.<<(adapter : Granite::Adapter::Base) : Nil\n      raise \"Adapter with name '#{adapter.name}' has already been registered.\" if @@registered_connections.any? { |conn| conn[:writer].name == adapter.name }\n      @@registered_connections << {writer: adapter, reader: adapter}\n    end\n\n    def self.<<(data : NamedTuple(name: String, reader: String, writer: String, adapter_type: Granite::Adapter::Base.class)) : Nil\n      raise \"Adapter with name '#{data[:name]}' has already been registered.\" if @@registered_connections.any? { |conn| conn[:writer].name == data[:name] }\n\n      writer_adapter = data[:adapter_type].new(name: data[:name], url: data[:writer])\n\n      # if reader/writer reference the same db. Make them point to the same granite adapter.\n      # This avoids connection pool duplications on the same database.\n      if data[:reader] == data[:writer]\n        return @@registered_connections << {writer: writer_adapter, reader: writer_adapter}\n      end\n\n      reader_adapter = data[:adapter_type].new(name: data[:name], url: data[:reader])\n      @@registered_connections << {writer: writer_adapter, reader: reader_adapter}\n    end\n\n    # Returns a registered connection with the given *name*, otherwise `nil`.\n    def self.[](name : String) : {writer: Granite::Adapter::Base, reader: Granite::Adapter::Base}?\n      registered_connections.find { |conn| conn[:writer].name == name }\n    end\n\n    def self.first_connection\n      first_connection = @@registered_connections.first?\n      raise \"First registered connection cannot be nil\" if first_connection.nil?\n      first_connection\n    end\n\n    def self.first_writer : Granite::Adapter::Base\n      first_connection[:writer]\n    end\n\n    def self.first_reader : Granite::Adapter::Base\n      first_connection[:reader]\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/converters.cr",
    "content": "module Granite::Converters\n  # Converts a `UUID` to/from a database column of type `T`.\n  #\n  # Valid types for `T` include: `String`, and `Bytes`.\n  module Uuid(T)\n    extend self\n\n    def to_db(value : ::UUID?) : Granite::Columns::Type\n      return nil if value.nil?\n      {% if T == String %}\n        value.to_s\n      {% elsif T == Bytes %}\n        # we need a heap allocated slice\n        v = value.bytes.each.to_a\n        Slice.new(v.to_unsafe, v.size)\n      {% else %}\n        {% raise \"#{@type.name}#to_db does not support #{T} yet.\" %}\n      {% end %}\n    end\n\n    def from_rs(result : ::DB::ResultSet) : ::UUID?\n      value = result.read(T?)\n      return nil if value.nil?\n      {% if T == String || T == Bytes %}\n        ::UUID.new value\n      {% else %}\n        {% raise \"#{@type.name}#from_rs does not support #{T} yet.\" %}\n      {% end %}\n    end\n  end\n\n  # Converts an Enum of type `E` to/from a database column of type `T`.\n  #\n  # Valid types for `T` include: `Number`, `String`, and `Bytes`.\n  module Enum(E, T)\n    extend self\n\n    def to_db(value : E?) : Granite::Columns::Type\n      return nil if value.nil?\n      {% if T <= Number %}\n        value.to_i64\n      {% elsif T == String || T == Bytes %}\n        value.to_s\n      {% else %}\n        {% raise \"#{@type.name}#to_db does not support #{T} yet.\" %}\n      {% end %}\n    end\n\n    def from_rs(result : ::DB::ResultSet) : E?\n      value = result.read(T?)\n      return nil if value.nil?\n      {% if T <= Number %}\n        E.from_value? value.to_i64\n      {% elsif T == String %}\n        E.parse? value\n      {% elsif T == Bytes %}\n        E.parse? String.new value\n      {% else %}\n        {% raise \"#{@type.name}#from_rs does not support #{T} yet.\" %}\n      {% end %}\n    end\n  end\n\n  # Converts an `Object` of type `M` to/from a database column of type `T`.\n  #\n  # Valid types for `T` include: `String`, `JSON::Any`, and `Bytes`.\n  #\n  # NOTE: `M` must implement `#to_json` and `.from_json` methods.\n  module Json(M, T)\n    extend self\n\n    def to_db(value : M?) : Granite::Columns::Type\n      return nil if value.nil?\n      {% if T == String || T == JSON::Any %}\n        value.to_json\n      {% elsif T == Bytes %}\n        value.to_json.to_slice\n      {% else %}\n        {% raise \"#{@type.name}#to_db does not support #{T} yet.\" %}\n      {% end %}\n    end\n\n    def from_rs(result : ::DB::ResultSet) : M?\n      value = result.read(T?)\n      return nil if value.nil?\n      {% if T == JSON::Any %}\n        M.from_json(value.to_json)\n      {% elsif T == String %}\n        M.from_json value\n      {% elsif T == Bytes %}\n        M.from_json String.new value\n      {% else %}\n        {% raise \"#{@type.name}#from_rs does not support #{T} yet.\" %}\n      {% end %}\n    end\n  end\n\n  # Converters a `PG::Numeric` value into a `Float64`.\n  module PgNumeric\n    extend self\n\n    def self.to_db(value) : Granite::Columns::Type\n      value ? value : nil\n    end\n\n    def self.from_rs(result : ::DB::ResultSet) : Float64?\n      result.read(::PG::Numeric?).try &.to_f\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/error.cr",
    "content": "class Granite::Error\n  property field, message\n\n  def initialize(@field : (String | Symbol | JSON::Any), @message : String? = \"\")\n  end\n\n  def to_json(builder : JSON::Builder)\n    builder.object do\n      builder.field \"field\", @field\n      builder.field \"message\", @message\n    end\n  end\n\n  def to_s(io)\n    if field == :base\n      io << message\n    else\n      io << field.to_s.capitalize << \" \" << message\n    end\n  end\nend\n\nclass Granite::ConversionError < Granite::Error\nend\n"
  },
  {
    "path": "src/granite/exceptions.cr",
    "content": "module Granite\n  class RecordNotSaved < ::Exception\n    getter model : Granite::Base\n\n    def initialize(class_name : String, @model : Granite::Base)\n      super(\"Could not process #{class_name}: #{model.errors.first.message}\")\n    end\n  end\n\n  class RecordNotDestroyed < ::Exception\n    getter model : Granite::Base\n\n    def initialize(class_name : String, @model : Granite::Base)\n      super(\"Could not destroy #{class_name}: #{model.errors.first.message}\")\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/integrators.cr",
    "content": "require \"./transactions\"\nrequire \"./querying\"\n\nmodule Granite::Integrators\n  include Transactions::ClassMethods\n  include Querying\n\n  def find_or_create_by(**args)\n    find_by(**args) || create(**args)\n  end\n\n  def find_or_initialize_by(**args)\n    find_by(**args) || new(**args)\n  end\nend\n"
  },
  {
    "path": "src/granite/migrator.cr",
    "content": "require \"./error\"\n\n# DB migration tool that prepares a table for the class\n#\n# ```\n# class User < Granite::Base\n#   adapter mysql\n#   field name : String\n# end\n#\n# User.migrator.drop_and_create\n# # => \"DROP TABLE IF EXISTS `users`;\"\n# # => \"CREATE TABLE `users` (id BIGSERIAL PRIMARY KEY, name VARCHAR(255));\"\n#\n# User.migrator(table_options: \"ENGINE=InnoDB DEFAULT CHARSET=utf8\").create\n# # => \"CREATE TABLE ... ENGINE=InnoDB DEFAULT CHARSET=utf8;\"\n# ```\nmodule Granite::Migrator\n  module ClassMethods\n    def migrator(**args)\n      Migrator(self).new(**args)\n    end\n  end\n\n  class Migrator(Model)\n    def initialize(@table_options = \"\")\n    end\n\n    def drop_and_create\n      drop\n      create\n    end\n\n    def drop_sql\n      \"DROP TABLE IF EXISTS #{Model.quoted_table_name};\"\n    end\n\n    def drop\n      Model.exec drop_sql\n    end\n\n    def create_sql\n      resolve = ->(key : String) {\n        Model.adapter.class.schema_type?(key) || raise \"Migrator(#{Model.adapter.class.name}) doesn't support '#{key}' yet.\"\n      }\n\n      String.build do |s|\n        s.puts \"CREATE TABLE #{Model.quoted_table_name}(\"\n\n        # primary key\n        {% begin %}\n          {% primary_key = Model.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n          {% raise raise \"A primary key must be defined for #{Model.name}.\" unless primary_key %}\n          {% ann = primary_key.annotation(Granite::Column) %}\n          k = Model.adapter.quote(\"{{primary_key.name}}\")\n          v =\n            {% if ann[:auto] %}\n              resolve.call(\"AUTO_{{primary_key.type.union_types.find { |t| t != Nil }.id}}\")\n            {% else %}\n              resolve.call(\"{{ivar.type.union_types.find { |t| t != Nil }.id}}\")\n            {% end %}\n          s.print \"#{k} #{v} PRIMARY KEY\"\n        {% end %}\n\n        # content fields\n        {% for ivar in Model.instance_vars.select { |ivar| (ann = ivar.annotation(Granite::Column)) && !ann[:primary] } %}\n          {% ann = ivar.annotation(Granite::Column) %}\n          s.puts \",\"\n          k = Model.adapter.quote(\"{{ivar.name}}\")\n          v =\n            {% if ann[:column_type] %}\n              \"{{ann[:column_type].id}}\"\n            {% elsif ivar.name.id == \"created_at\" || ivar.name.id == \"updated_at\" %}\n              resolve.call(\"{{ivar.name}}\")\n            {% elsif ann[:nilable] %}\n              resolve.call(\"{{ivar.type.union_types.find { |t| t != Nil }.id}}\")\n            {% else %}\n              resolve.call(\"{{ivar.type.union_types.find { |t| t != Nil }.id}}\") + \" NOT NULL\"\n            {% end %}\n          s.puts \"#{k} #{v}\"\n        {% end %}\n\n        s.puts \") #{@table_options};\"\n      end\n    end\n\n    def create\n      Model.exec create_sql\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/query/assemblers/base.cr",
    "content": "module Granite::Query::Assembler\n  abstract class Base(Model)\n    @placeholder : String = \"\"\n    @where : String?\n    @order : String?\n    @limit : String?\n    @offset : String?\n    @group_by : String?\n\n    def initialize(@query : Builder(Model))\n      @numbered_parameters = [] of Granite::Columns::Type\n      @aggregate_fields = [] of String\n    end\n\n    abstract def add_parameter(value : Granite::Columns::Type) : String\n\n    def numbered_parameters\n      @numbered_parameters\n    end\n\n    def add_aggregate_field(name : String)\n      @aggregate_fields << name\n    end\n\n    def table_name\n      Model.table_name\n    end\n\n    def field_list\n      [Model.fields].flatten.join \", \"\n    end\n\n    def build_sql(&)\n      clauses = [] of String?\n      yield clauses\n      clauses.compact!.join \" \"\n    end\n\n    def where\n      return @where if @where\n\n      clauses = [\"WHERE\"]\n\n      @query.where_fields.each do |expression|\n        clauses << expression[:join].to_s.upcase unless clauses.size == 1\n\n        if expression[:field]?.nil? # custom SQL\n          expression = expression.as(NamedTuple(join: Symbol, stmt: String, value: Granite::Columns::Type))\n\n          if !expression[:value].nil?\n            param_token = add_parameter expression[:value]\n            clause = expression[:stmt].gsub(@placeholder, param_token)\n          else\n            clause = expression[:stmt]\n          end\n\n          clauses << clause\n        else # standard where query\n          expression = expression.as(NamedTuple(join: Symbol, field: String, operator: Symbol, value: Granite::Columns::Type))\n          add_aggregate_field expression[:field]\n\n          if expression[:value].nil?\n            clauses << \"#{expression[:field]} IS NULL\"\n          elsif expression[:value].is_a?(Array)\n            in_stmt = String.build do |str|\n              str << '('\n              expression[:value].as(Array).each_with_index do |val, idx|\n                case val\n                when Bool, Number\n                  str << val\n                else\n                  str << add_parameter val\n                end\n                str << ',' if expression[:value].as(Array).size - 1 != idx\n              end\n              str << ')'\n            end\n            clauses << \"#{expression[:field]} #{sql_operator(expression[:operator])} #{in_stmt}\"\n          else\n            clauses << \"#{expression[:field]} #{sql_operator(expression[:operator])} #{add_parameter expression[:value]}\"\n          end\n        end\n      end\n\n      return nil if clauses.size == 1\n\n      @where = clauses.join(\" \")\n    end\n\n    def order(use_default_order = true)\n      return @order if @order\n\n      order_fields = @query.order_fields\n\n      if order_fields.none?\n        if use_default_order\n          order_fields = default_order\n        else\n          return nil\n        end\n      end\n\n      order_clauses = order_fields.map do |expression|\n        add_aggregate_field expression[:field]\n\n        if expression[:direction] == Builder::Sort::Ascending\n          \"#{expression[:field]} ASC\"\n        else\n          \"#{expression[:field]} DESC\"\n        end\n      end\n\n      @order = \"ORDER BY #{order_clauses.join \", \"}\"\n    end\n\n    def group_by\n      return @group_by if @group_by\n      group_fields = @query.group_fields\n      return nil if group_fields.none?\n      group_clauses = group_fields.map do |expression|\n        \"#{expression[:field]}\"\n      end\n\n      @group_by = \"GROUP BY #{group_clauses.join \", \"}\"\n    end\n\n    def limit\n      @limit ||= if limit = @query.limit\n                   \"LIMIT #{limit}\"\n                 end\n    end\n\n    def offset\n      @offset ||= if offset = @query.offset\n                    \"OFFSET #{offset}\"\n                  end\n    end\n\n    def log(*stuff)\n    end\n\n    def default_order\n      [{field: Model.primary_name, direction: \"ASC\"}]\n    end\n\n    def count : (Executor::MultiValue(Model, Int64) | Executor::Value(Model, Int64))\n      sql = build_sql do |s|\n        s << \"SELECT COUNT(*)\"\n        s << \"FROM #{table_name}\"\n        s << where\n        s << group_by\n        s << order(use_default_order: false)\n        s << limit\n        s << offset\n      end\n\n      if group_by\n        Executor::MultiValue(Model, Int64).new sql, numbered_parameters, default: 0_i64\n      else\n        Executor::Value(Model, Int64).new sql, numbered_parameters, default: 0_i64\n      end\n    end\n\n    def first(n : Int32 = 1) : Executor::List(Model)\n      sql = build_sql do |s|\n        s << \"SELECT #{field_list}\"\n        s << \"FROM #{table_name}\"\n        s << where\n        s << group_by\n        s << order\n        s << \"LIMIT #{n}\"\n        s << offset\n      end\n\n      Executor::List(Model).new sql, numbered_parameters\n    end\n\n    def delete\n      sql = build_sql do |s|\n        s << \"DELETE FROM #{table_name}\"\n        s << where\n      end\n\n      log sql, numbered_parameters\n      Model.adapter.open do |db|\n        db.exec sql, args: numbered_parameters\n      end\n    end\n\n    def select\n      sql = build_sql do |s|\n        s << \"SELECT #{field_list}\"\n        s << \"FROM #{table_name}\"\n        s << where\n        s << group_by\n        s << order\n        s << limit\n        s << offset\n      end\n\n      Executor::List(Model).new sql, numbered_parameters\n    end\n\n    def exists? : Executor::Value(Model, Bool)\n      sql = build_sql do |s|\n        s << \"SELECT EXISTS(SELECT 1 \"\n        s << \"FROM #{table_name} \"\n        s << where\n        s << \")\"\n      end\n\n      Executor::Value(Model, Bool).new sql, numbered_parameters, default: false\n    end\n\n    OPERATORS = {\"eq\": \"=\", \"gteq\": \">=\", \"lteq\": \"<=\", \"neq\": \"!=\", \"ltgt\": \"<>\", \"gt\": \">\", \"lt\": \"<\", \"ngt\": \"!>\", \"nlt\": \"!<\", \"in\": \"IN\", \"nin\": \"NOT IN\", \"like\": \"LIKE\", \"nlike\": \"NOT LIKE\"}\n\n    def sql_operator(operator : Symbol) : String\n      OPERATORS[operator.to_s]? || operator.to_s\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/query/assemblers/mysql.cr",
    "content": "# Query runner which finalizes a query and runs it.\n# This will likely require adapter specific subclassing :[.\nmodule Granite::Query::Assembler\n  class Mysql(Model) < Base(Model)\n    @placeholder = \"?\"\n\n    def add_parameter(value : Granite::Columns::Type) : String\n      @numbered_parameters << value\n      \"?\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/query/assemblers/pg.cr",
    "content": "# Query runner which finalizes a query and runs it.\n# This will likely require adapter specific subclassing :[.\nmodule Granite::Query::Assembler\n  class Pg(Model) < Base(Model)\n    @placeholder = \"$\"\n\n    def add_parameter(value : Granite::Columns::Type) : String\n      @numbered_parameters << value\n      \"$#{@numbered_parameters.size}\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/query/assemblers/sqlite.cr",
    "content": "module Granite::Query::Assembler\n  class Sqlite(Model) < Base(Model)\n    @placeholder = \"?\"\n\n    def add_parameter(value : Granite::Columns::Type) : String\n      @numbered_parameters << value\n      \"?\"\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/query/builder.cr",
    "content": "# Data structure which will allow chaining of query components,\n# nesting of boolean logic, etc.\n#\n# Should return self, or another instance of Builder wherever\n# chaining should be possible.\n#\n# Current query syntax:\n# - where(field: value) => \"WHERE field = 'value'\"\n#\n# Hopefully soon:\n# - Model.where(field: value).not( Model.where(field2: value2) )\n# or\n# - Model.where(field: value).not { where(field2: value2) }\n#\n# - Model.where(field: value).or( Model.where(field3: value3) )\n# or\n# - Model.where(field: value).or { whehre(field3: value3) }\nclass Granite::Query::Builder(Model)\n  enum DbType\n    Mysql\n    Sqlite\n    Pg\n  end\n\n  enum Sort\n    Ascending\n    Descending\n  end\n\n  getter db_type : DbType\n  getter where_fields = [] of (NamedTuple(join: Symbol, field: String, operator: Symbol, value: Granite::Columns::Type) |\n                               NamedTuple(join: Symbol, stmt: String, value: Granite::Columns::Type))\n  getter order_fields = [] of NamedTuple(field: String, direction: Sort)\n  getter group_fields = [] of NamedTuple(field: String)\n  getter offset : Int64?\n  getter limit : Int64?\n\n  def initialize(@db_type, @boolean_operator = :and)\n  end\n\n  def assembler : Assembler::Base(Model)\n    case @db_type\n    when DbType::Pg\n      Assembler::Pg(Model).new self\n    when DbType::Mysql\n      Assembler::Mysql(Model).new self\n    when DbType::Sqlite\n      Assembler::Sqlite(Model).new self\n    else\n      raise \"Database type not supported\"\n    end\n  end\n\n  def where(**matches)\n    where(matches)\n  end\n\n  def where(matches)\n    matches.each do |field, value|\n      if value.is_a?(Array)\n        and(field: field.to_s, operator: :in, value: value.compact)\n      elsif value.is_a?(Enum)\n        and(field: field.to_s, operator: :eq, value: value.to_s)\n      else\n        and(field: field.to_s, operator: :eq, value: value)\n      end\n    end\n\n    self\n  end\n\n  def where(field : (Symbol | String), operator : Symbol, value : Granite::Columns::Type)\n    and(field: field.to_s, operator: operator, value: value)\n  end\n\n  def where(stmt : String, value : Granite::Columns::Type = nil)\n    and(stmt: stmt, value: value)\n  end\n\n  def and(field : (Symbol | String), operator : Symbol, value : Granite::Columns::Type)\n    @where_fields << {join: :and, field: field.to_s, operator: operator, value: value}\n\n    self\n  end\n\n  def and(stmt : String, value : Granite::Columns::Type = nil)\n    @where_fields << {join: :and, stmt: stmt, value: value}\n\n    self\n  end\n\n  def and(**matches)\n    and(matches)\n  end\n\n  def and(matches)\n    matches.each do |field, value|\n      if value.is_a?(Array)\n        and(field: field.to_s, operator: :in, value: value.compact)\n      elsif value.is_a?(Enum)\n        and(field: field.to_s, operator: :eq, value: value.to_s)\n      else\n        and(field: field.to_s, operator: :eq, value: value)\n      end\n    end\n    self\n  end\n\n  def or(**matches)\n    or(matches)\n  end\n\n  def or(matches)\n    matches.each do |field, value|\n      if value.is_a?(Array)\n        or(field: field.to_s, operator: :in, value: value.compact)\n      elsif value.is_a?(Enum)\n        or(field: field.to_s, operator: :eq, value: value.to_s)\n      else\n        or(field: field.to_s, operator: :eq, value: value)\n      end\n    end\n    self\n  end\n\n  def or(field : (Symbol | String), operator : Symbol, value : Granite::Columns::Type)\n    @where_fields << {join: :or, field: field.to_s, operator: operator, value: value}\n\n    self\n  end\n\n  def or(stmt : String, value : Granite::Columns::Type = nil)\n    @where_fields << {join: :or, stmt: stmt, value: value}\n\n    self\n  end\n\n  def order(field : Symbol)\n    @order_fields << {field: field.to_s, direction: Sort::Ascending}\n\n    self\n  end\n\n  def order(fields : Array(Symbol))\n    fields.each do |field|\n      order field\n    end\n\n    self\n  end\n\n  def order(**dsl)\n    order(dsl)\n  end\n\n  def order(dsl)\n    dsl.each do |field, dsl_direction|\n      direction = Sort::Ascending\n\n      if dsl_direction == \"desc\" || dsl_direction == :desc\n        direction = Sort::Descending\n      end\n\n      @order_fields << {field: field.to_s, direction: direction}\n    end\n\n    self\n  end\n\n  def group_by(field : Symbol)\n    @group_fields << {field: field.to_s}\n\n    self\n  end\n\n  def group_by(fields : Array(Symbol))\n    fields.each do |field|\n      group_by field\n    end\n\n    self\n  end\n\n  def group_by(**dsl)\n    group_by(dsl)\n  end\n\n  def group_by(dsl)\n    dsl.each do |field|\n      @group_fields << {field: field.to_s}\n    end\n\n    self\n  end\n\n  def offset(num)\n    @offset = num.to_i64\n\n    self\n  end\n\n  def limit(num)\n    @limit = num.to_i64\n\n    self\n  end\n\n  def raw_sql\n    assembler.select.raw_sql\n  end\n\n  # TODO: replace `querying.first` with this\n  # def first : Model?\n  #   first(1).first?\n  # end\n\n  # def first(n : Int32) : Executor::List(Model)\n  #   assembler.first(n)\n  # end\n\n  def any? : Bool\n    !first.nil?\n  end\n\n  def delete\n    Model.switch_to_writer_adapter\n    assembler.delete\n  end\n\n  def select\n    assembler.select.run\n  end\n\n  def count\n    assembler.count\n  end\n\n  def exists? : Bool\n    assembler.exists?.run\n  end\n\n  def size\n    count\n  end\n\n  def reject(&)\n    assembler.select.run.reject do |record|\n      yield record\n    end\n  end\n\n  def each(&)\n    assembler.select.tap do |record_set|\n      record_set.each do |record|\n        yield record\n      end\n    end\n  end\n\n  def map(&)\n    assembler.select.run.map do |record|\n      yield record\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/query/builder_methods.cr",
    "content": "module Granite::Query::BuilderMethods\n  def __builder\n    db_type = case adapter.class.to_s\n              when \"Granite::Adapter::Pg\"\n                Granite::Query::Builder::DbType::Pg\n              when \"Granite::Adapter::Mysql\"\n                Granite::Query::Builder::DbType::Mysql\n              else\n                Granite::Query::Builder::DbType::Sqlite\n              end\n\n    Builder(self).new(db_type)\n  end\n\n  delegate where, order, offset, limit, to: __builder\nend\n"
  },
  {
    "path": "src/granite/query/executors/base.cr",
    "content": "module Granite::Query::Executor\n  module Shared\n    def raw_sql : String\n      @sql\n    end\n\n    def log(*messages)\n      messages.each { |message| Log.debug { message } }\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/query/executors/list.cr",
    "content": "module Granite::Query::Executor\n  class List(Model)\n    include Shared\n\n    def initialize(@sql : String, @args = [] of Granite::Columns::Type)\n    end\n\n    def run : Array(Model)\n      log @sql, @args\n\n      results = [] of Model\n\n      Model.adapter.open do |db|\n        db.query @sql, args: @args do |record_set|\n          record_set.each do\n            results << Model.from_rs record_set\n          end\n        end\n      end\n\n      results\n    end\n\n    delegate :[], :first?, :first, :each, :group_by, to: :run\n    delegate :to_s, to: :run\n  end\nend\n"
  },
  {
    "path": "src/granite/query/executors/multi_value.cr",
    "content": "module Granite::Query::Executor\n  class MultiValue(Model, Scalar)\n    include Shared\n\n    def initialize(@sql : String, @args = [] of Granite::Columns::Type, @default : Scalar = nil)\n    end\n\n    def run : Array(Scalar)\n      log @sql, @args\n\n      raise \"No default provided\" if @default.nil?\n      results = [] of Scalar\n\n      Model.adapter.open do |db|\n        db.query @sql, args: @args do |record_set|\n          record_set.each do\n            results << record_set.read(Scalar)\n          end\n        end\n      end\n\n      results\n    end\n\n    delegate :to_i, :to_s, to: :run\n    delegate :<, :>, :<=, :>=, to: :run\n  end\nend\n"
  },
  {
    "path": "src/granite/query/executors/value.cr",
    "content": "module Granite::Query::Executor\n  class Value(Model, Scalar)\n    include Shared\n\n    def initialize(@sql : String, @args = [] of Granite::Columns::Type, @default : Scalar = nil)\n    end\n\n    def run : Scalar\n      log @sql, @args\n      # db.scalar raises when a query returns 0 results, so I'm using query_one?\n      # https://github.com/crystal-lang/crystal-db/blob/7d30e9f50e478cb6404d16d2ce91e639b6f9c476/src/db/statement.cr#L18\n\n      if @default.nil?\n        raise \"No default provided\"\n      else\n        Model.adapter.open do |db|\n          db.query_one?(@sql, args: @args, as: Scalar) || @default\n        end\n      end\n    end\n\n    delegate :<, :>, :<=, :>=, to: :run\n    delegate :to_i, :to_s, to: :run\n  end\nend\n"
  },
  {
    "path": "src/granite/querying.cr",
    "content": "module Granite::Querying\n  class NotFound < Exception\n  end\n\n  module ClassMethods\n    # Entrypoint for creating a new object from a result set.\n    def from_rs(result : DB::ResultSet) : self\n      model = new\n      model.new_record = false\n      model.from_rs result\n      model\n    end\n\n    def raw_all(clause = \"\", params = [] of Granite::Columns::Type)\n      rows = [] of self\n      adapter.select(select_container, clause, params) do |results|\n        results.each do\n          rows << from_rs(results)\n        end\n      end\n      rows\n    end\n\n    # All will return all rows in the database. The clause allows you to specify\n    # a WHERE, JOIN, GROUP BY, ORDER BY and any other SQL92 compatible query to\n    # your table. The result will be a Collection(Model) object which lazy loads\n    # an array of instantiated instances of your Model class.\n    # This allows you to take full advantage of the database\n    # that you are using so you are not restricted or dummied down to support a\n    # DSL.\n    # Lazy load prevent running unnecessary queries from unused variables.\n    def all(clause = \"\", params = [] of Granite::Columns::Type, use_primary_adapter = true)\n      switch_to_writer_adapter if use_primary_adapter == true\n      Collection(self).new(->{ raw_all(clause, params) })\n    end\n\n    # First adds a `LIMIT 1` clause to the query and returns the first result\n    def first(clause = \"\", params = [] of Granite::Columns::Type)\n      all([clause.strip, \"LIMIT 1\"].join(\" \"), params, false).first?\n    end\n\n    def first!(clause = \"\", params = [] of Granite::Columns::Type)\n      first(clause, params) || raise NotFound.new(\"No #{{{@type.name.stringify}}} found with first(#{clause})\")\n    end\n\n    # find returns the row with the primary key specified. Otherwise nil.\n    def find(value)\n      first(\"WHERE #{primary_name} = ?\", [value])\n    end\n\n    # find returns the row with the primary key specified. Otherwise raises an exception.\n    def find!(value)\n      find(value) || raise Granite::Querying::NotFound.new(\"No #{{{@type.name.stringify}}} found where #{primary_name} = #{value}\")\n    end\n\n    # Returns the first row found that matches *criteria*. Otherwise `nil`.\n    def find_by(**criteria : Granite::Columns::Type)\n      find_by criteria.to_h\n    end\n\n    # :ditto:\n    def find_by(criteria : Granite::ModelArgs)\n      clause, params = build_find_by_clause(criteria)\n      first \"WHERE #{clause}\", params\n    end\n\n    # Returns the first row found that matches *criteria*. Otherwise raises a `NotFound` exception.\n    def find_by!(**criteria : Granite::Columns::Type)\n      find_by!(criteria.to_h)\n    end\n\n    # :ditto:\n    def find_by!(criteria : Granite::ModelArgs)\n      find_by(criteria) || raise NotFound.new(\"No #{{{@type.name.stringify}}} found where #{criteria.map { |k, v| %(#{k} #{v.nil? ? \"is NULL\" : \"= #{v}\"}) }.join(\" and \")}\")\n    end\n\n    def find_each(clause = \"\", params = [] of Granite::Columns::Type, batch_size limit = 100, offset = 0, &)\n      find_in_batches(clause, params, batch_size: limit, offset: offset) do |batch|\n        batch.each do |record|\n          yield record\n        end\n      end\n    end\n\n    def find_in_batches(clause = \"\", params = [] of Granite::Columns::Type, batch_size limit = 100, offset = 0, &)\n      if limit < 1\n        raise ArgumentError.new(\"batch_size must be >= 1\")\n      end\n\n      loop do\n        results = all \"#{clause} LIMIT ? OFFSET ?\", params + [limit, offset], false\n        break if results.empty?\n        yield results\n        offset += limit\n      end\n    end\n\n    # Returns `true` if a records exists with a PK of *id*, otherwise `false`.\n    def exists?(id : Number | String | Nil) : Bool\n      return false if id.nil?\n      exec_exists \"#{primary_name} = ?\", [id]\n    end\n\n    # Returns `true` if a records exists that matches *criteria*, otherwise `false`.\n    def exists?(**criteria : Granite::Columns::Type) : Bool\n      exists? criteria.to_h\n    end\n\n    # :ditto:\n    def exists?(criteria : Granite::ModelArgs) : Bool\n      exec_exists *build_find_by_clause(criteria)\n    end\n\n    # count returns a count of all the records\n    def count : Int32\n      scalar \"SELECT COUNT(*) FROM #{quoted_table_name}\", &.to_s.to_i\n    end\n\n    def exec(clause = \"\")\n      switch_to_writer_adapter\n      adapter.open(&.exec(clause))\n    end\n\n    def query(clause = \"\", params = [] of Granite::Columns::Type, &)\n      switch_to_writer_adapter\n      adapter.open { |db| yield db.query(clause, args: params) }\n    end\n\n    def scalar(clause = \"\", &)\n      switch_to_writer_adapter\n      adapter.open { |db| yield db.scalar(clause) }\n    end\n\n    private def exec_exists(clause : String, params : Array(Granite::Columns::Type)) : Bool\n      self.adapter.exists? quoted_table_name, clause, params\n    end\n\n    private def build_find_by_clause(criteria : Granite::ModelArgs)\n      keys = criteria.keys\n      criteria_hash = criteria.dup\n\n      clauses = keys.map do |name|\n        if criteria_hash.has_key?(name) && !criteria_hash[name].nil?\n          matcher = \"= ?\"\n        else\n          matcher = \"IS NULL\"\n          criteria_hash.delete name\n        end\n\n        \"#{quoted_table_name}.#{quote(name.to_s)} #{matcher}\"\n      end\n\n      {clauses.join(\" AND \"), criteria_hash.values}\n    end\n  end\n\n  # Returns the record with the attributes reloaded from the database.\n  #\n  # **Note:** this method is only defined when the `Spec` module is present.\n  #\n  # ```\n  # post = Post.create(name: \"Granite Rocks!\", body: \"Check this out.\")\n  # # record gets updated by another process\n  # post.reload # performs another find to fetch the record again\n  # ```\n  def reload\n    {% if !@top_level.has_constant? \"Spec\" %}\n      raise \"#reload is a convenience method for testing only, please use #find in your application code\"\n    {% end %}\n    self.class.find!(primary_key_value)\n  end\nend\n"
  },
  {
    "path": "src/granite/select.cr",
    "content": "module Granite::Select\n  struct Container\n    property custom : String?\n    getter table_name, fields\n\n    def initialize(@custom = nil, @table_name = \"\", @fields = [] of String)\n    end\n  end\n\n  macro select_statement(text)\n    @@select_container.custom = {{text.strip}}\n\n    def self.select : String?\n      self.select_container.custom\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/settings.cr",
    "content": "module Granite\n  class Settings\n    property default_timezone : Time::Location = Time::Location.load(Granite::TIME_ZONE)\n\n    def default_timezone=(name : String)\n      @default_timezone = Time::Location.load(name)\n    end\n  end\n\n  def self.settings\n    @@settings ||= Settings.new\n  end\nend\n"
  },
  {
    "path": "src/granite/table.cr",
    "content": "# Adds a :nodoc: to granite methods/constants if `DISABLE_GRANTE_DOCS` ENV var is true\nmacro disable_granite_docs?(stmt)\n  {% unless flag?(:granite_docs) %}\n    # :nodoc:\n    {{stmt.id}}\n  {% else %}\n    {{stmt.id}}\n  {% end %}\nend\n\nmodule Granite::Tables\n  module ClassMethods\n    def primary_name\n      {% begin %}\n      {% primary_key = @type.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n      {% if pk = primary_key %}\n        {{pk.name.stringify}}\n      {% end %}\n    {% end %}\n    end\n\n    def primary_type\n      {% begin %}\n      {% primary_key = @type.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n      {% if pk = primary_key %}\n        {{pk.type}}\n      {% end %}\n    {% end %}\n    end\n\n    def quoted_table_name : String\n      self.adapter.quote(table_name)\n    end\n\n    def quote(column_name) : String\n      self.adapter.quote(column_name)\n    end\n\n    # Returns the name of the table for `self`\n    # defaults to the model's name underscored + 's'.\n    def table_name : String\n      {% begin %}\n        {% table_ann = @type.annotation(Granite::Table) %}\n        {{table_ann && !table_ann[:name].nil? ? table_ann[:name] : @type.name.underscore.stringify.split(\"::\").last}}\n      {% end %}\n    end\n  end\n\n  macro table(name)\n    @[Granite::Table(name: {{(name.is_a?(StringLiteral) ? name : name.id.stringify) || nil}})]\n    class ::{{@type.name.id}}; end\n  end\nend\n"
  },
  {
    "path": "src/granite/transactions.cr",
    "content": "require \"./exceptions\"\n\nmodule Granite::Transactions\n  module ClassMethods\n    # Removes all records from a table.\n    def clear\n      adapter.clear table_name\n    end\n\n    # Creates a new record, and attempts to save it to the database. Returns the\n    # newly created record.\n    #\n    # **NOTE**: This method still outputs the new object even when it failed to save\n    # to the database. The only way to determine a failure is to check any errors on\n    # the object, or to use `#create!`.\n    def create(**args)\n      create(args.to_h)\n    end\n\n    # Creates a new record, and attempts to save it to the database. Allows saving\n    # the record without timestamps. Returns the newly created record.\n    def create(args, skip_timestamps : Bool = false)\n      instance = new\n      instance.set_attributes(args.to_h.transform_keys(&.to_s))\n      instance.save(skip_timestamps: skip_timestamps)\n      instance\n    end\n\n    # Creates a new record, and attempts to save it to the database. Returns the\n    # newly created record. Raises `Granite::RecordNotSaved` if the save is\n    # unsuccessful.\n    def create!(**args)\n      create!(args.to_h)\n    end\n\n    # Creates a new record, and attempts to save it to the database. Allows saving\n    # the record without timestamps. Returns the newly created record. Raises\n    # `Granite::RecordNotSaved` if the save is unsuccessful.\n    def create!(args, skip_timestamps : Bool = false)\n      instance = create(args, skip_timestamps)\n\n      unless instance.errors.empty?\n        raise Granite::RecordNotSaved.new(self.name, instance)\n      end\n\n      instance\n    end\n\n    # Runs an INSERT statement for all records in *model_array*.\n    # the array must contain only one model class\n    # invalid model records will be skipped\n    def import(model_array : Array(self) | Granite::Collection(self), batch_size : Int32 = model_array.size)\n      {% begin %}\n        {% primary_key = @type.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n        {% raise raise \"A primary key must be defined for #{@type.name}.\" unless primary_key %}\n        {% ann = primary_key.annotation(Granite::Column) %}\n        fields_duplicate = fields.dup\n        model_array.each_slice(batch_size, true) do |slice|\n          slice.each do |i|\n            i.before_save\n            i.before_create\n          end\n          adapter.import(table_name, {{primary_key.name.stringify}}, {{ann[:auto]}}, fields_duplicate, slice)\n          slice.each do |i|\n            i.after_create\n            i.after_save\n          end\n        end\n      {% end %}\n    rescue err\n      raise DB::Error.new(err.message, cause: err)\n    end\n\n    # Runs an INSERT statement for all records in *model_array*, with options to\n    # update any duplicate records, and provide column names.\n    def import(model_array : Array(self) | Granite::Collection(self), update_on_duplicate : Bool, columns : Array(String), batch_size : Int32 = model_array.size)\n      {% begin %}\n        {% primary_key = @type.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n        {% raise raise \"A primary key must be defined for #{@type.name}.\" unless primary_key %}\n        {% ann = primary_key.annotation(Granite::Column) %}\n        fields_duplicate = fields.dup\n        model_array.each_slice(batch_size, true) do |slice|\n          slice.each do |i|\n            i.before_save\n            i.before_create\n          end\n          adapter.import(table_name, {{primary_key.name.stringify}}, {{ann[:auto]}}, fields_duplicate, slice, update_on_duplicate: update_on_duplicate, columns: columns)\n          slice.each do |i|\n            i.after_create\n            i.after_save\n          end\n        end\n      {% end %}\n    rescue err\n      raise DB::Error.new(err.message, cause: err)\n    end\n\n    def import(model_array : Array(self) | Granite::Collection(self), ignore_on_duplicate : Bool, batch_size : Int32 = model_array.size)\n      {% begin %}\n        {% primary_key = @type.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n        {% raise raise \"A primary key must be defined for #{@type.name}.\" unless primary_key %}\n        {% ann = primary_key.annotation(Granite::Column) %}\n        fields_duplicate = fields.dup\n        model_array.each_slice(batch_size, true) do |slice|\n          slice.each do |i|\n            i.before_save\n            i.before_create\n          end\n          adapter.import(table_name, {{primary_key.name.stringify}}, {{ann[:auto]}}, fields_duplicate, slice, ignore_on_duplicate: ignore_on_duplicate)\n          slice.each do |i|\n            i.after_create\n            i.after_save\n          end\n        end\n      {% end %}\n    rescue err\n      raise DB::Error.new(err.message, cause: err)\n    end\n  end\n\n  # Sets the record's timestamps(created_at & updated_at) to the current time.\n  def set_timestamps(*, to time = Time.local(Granite.settings.default_timezone), mode = :create)\n    {% if @type.instance_vars.select { |ivar| ivar.annotation(Granite::Column) && ivar.type == Time? }.map(&.name.stringify).includes? \"created_at\" %}\n      if mode == :create\n        @created_at = time.at_beginning_of_second\n      end\n    {% end %}\n\n    {% if @type.instance_vars.select { |ivar| ivar.annotation(Granite::Column) && ivar.type == Time? }.map(&.name.stringify).includes? \"updated_at\" %}\n      @updated_at = time.at_beginning_of_second\n    {% end %}\n  end\n\n  private def __create(skip_timestamps : Bool = false)\n    {% begin %}\n      {% primary_key = @type.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n      {% raise raise \"A primary key must be defined for #{@type.name}.\" unless primary_key %}\n      {% raise \"Composite primary keys are not yet supported for '#{@type.name}'.\" if @type.instance_vars.select { |ivar| ann = ivar.annotation(Granite::Column); ann && ann[:primary] }.size > 1 %}\n      {% ann = primary_key.annotation(Granite::Column) %}\n\n      set_timestamps unless skip_timestamps\n      fields = self.class.content_fields.dup\n      params = content_values\n\n      if value = @{{primary_key.name.id}}\n        fields << {{primary_key.name.stringify}}\n        params << value\n      end\n\n      {% if primary_key.type == Int32? && ann[:auto] == true %}\n        @{{primary_key.name.id}} = self.class.adapter.insert(self.class.table_name, fields, params, lastval: {{primary_key.name.stringify}}).to_i32\n      {% elsif primary_key.type == Int64? && ann[:auto] == true %}\n        @{{primary_key.name.id}} = self.class.adapter.insert(self.class.table_name, fields, params, lastval: {{primary_key.name.stringify}})\n      {% elsif primary_key.type == UUID? && ann[:auto] == true %}\n          # if the primary key has not been set, then do so\n\n          unless fields.includes?({{primary_key.name.stringify}})\n            _uuid = UUID.random\n            @{{primary_key.name.id}} = _uuid\n            params << _uuid\n            fields << {{primary_key.name.stringify}}\n          end\n          self.class.adapter.insert(self.class.table_name, fields, params, lastval: nil)\n      {% elsif ann[:auto] == true %}\n        {% raise \"Failed to define #{@type.name}#save: Primary key must be Int(32|64) or UUID, or set `auto: false` for natural keys.\\n\\n  column #{primary_key.name} : #{primary_key.type}, primary: true, auto: false\\n\" %}\n      {% else %}\n        if @{{primary_key.name.id}}\n          self.class.adapter.insert(self.class.table_name, fields, params, lastval: nil)\n        else\n          message = \"Primary key('{{primary_key.name}}') cannot be null\"\n          errors << Granite::Error.new({{primary_key.name.stringify}}, message)\n          raise DB::Error.new\n        end\n      {% end %}\n    {% end %}\n  rescue err : DB::Error\n    raise err\n  rescue err\n    raise DB::Error.new(err.message, cause: err)\n  else\n    self.new_record = false\n  end\n\n  private def __update(skip_timestamps : Bool = false)\n    {% begin %}\n    {% primary_key = @type.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n    {% raise raise \"A primary key must be defined for #{@type.name}.\" unless primary_key %}\n    {% ann = primary_key.annotation(Granite::Column) %}\n    set_timestamps(mode: :update) unless skip_timestamps\n    fields = self.class.content_fields.dup\n    params = content_values + [@{{primary_key.name.id}}]\n\n    # Do not update created_at on update\n    if created_at_index = fields.index(\"created_at\")\n      fields.delete_at created_at_index\n      params.delete_at created_at_index\n    end\n\n    begin\n     self.class.adapter.update(self.class.table_name, self.class.primary_name, fields, params)\n    rescue err\n      raise DB::Error.new(err.message, cause: err)\n    end\n  {% end %}\n  end\n\n  private def __destroy\n    {% begin %}\n    {% primary_key = @type.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n    {% raise raise \"A primary key must be defined for #{@type.name}.\" unless primary_key %}\n    {% ann = primary_key.annotation(Granite::Column) %}\n    self.class.adapter.delete(self.class.table_name, self.class.primary_name, @{{primary_key.name.id}})\n    @destroyed = true\n  {% end %}\n  end\n\n  # Attempts to save the record to the database, returning `true` if successful,\n  # and `false` if not. If the save is unsuccessful, `#errors` will be populated\n  # with the errors which caused the save to fail.\n  #\n  # **NOTE**: This method can be used both on new records, and existing records.\n  # In the case of new records, it creates the record in the database, otherwise,\n  # it updates the record in the database.\n  def save(*, validate : Bool = true, skip_timestamps : Bool = false)\n    {% begin %}\n    {% primary_key = @type.instance_vars.find { |ivar| (ann = ivar.annotation(Granite::Column)) && ann[:primary] } %}\n    {% raise raise \"A primary key must be defined for #{@type.name}.\" unless primary_key %}\n    {% ann = primary_key.annotation(Granite::Column) %}\n    return false if validate && !valid?\n\n    begin\n      __before_save\n      if @{{primary_key.name.id}} && !new_record?\n        __before_update\n        __update(skip_timestamps: skip_timestamps)\n        __after_update\n      else\n        __before_create\n        __create(skip_timestamps: skip_timestamps)\n        __after_create\n      end\n      __after_save\n    rescue ex : DB::Error | Granite::Callbacks::Abort\n      if message = ex.message\n        Log.error { \"Save Exception: #{message}\" }\n        errors << Granite::Error.new(:base, message)\n      end\n      return false\n    end\n    true\n  {% end %}\n  end\n\n  # Same as `#save`, but raises `Granite::RecordNotSaved` if the save is unsuccessful.\n  def save!(*, validate : Bool = true, skip_timestamps : Bool = false)\n    save(validate: validate, skip_timestamps: skip_timestamps) || raise Granite::RecordNotSaved.new(self.class.name, self)\n  end\n\n  # Updates the record with the new data specified by *args*. Returns `true` if the\n  # update is successful, `false` if it isn't.\n  def update(**args)\n    update(args.to_h)\n  end\n\n  # Updates the record with the new data specified by *args*, with the option to\n  # not update timestamps. Returns `true` if the update is successful, `false` if\n  # it isn't.\n  def update(args, skip_timestamps : Bool = false)\n    set_attributes(args.to_h.transform_keys(&.to_s))\n\n    save(skip_timestamps: skip_timestamps)\n  end\n\n  # Updates the record with the new data specified by *args*. Raises\n  # `Granite::RecordNotSaved` if the save is unsuccessful.\n  def update!(**args)\n    update!(args.to_h)\n  end\n\n  # Updates the record with the new data specified by *args*, with the option to\n  # not update timestamps. Raises `Granite::RecordNotSaved` if the save is\n  # unsuccessful.\n  def update!(args, skip_timestamps : Bool = false)\n    set_attributes(args.to_h.transform_keys(&.to_s))\n\n    save!(skip_timestamps: skip_timestamps)\n  end\n\n  # Removes the record from the database. Returns `true` if successful, `false`\n  # otherwise.\n  def destroy\n    begin\n      __before_destroy\n      __destroy\n      __after_destroy\n    rescue ex : DB::Error | Granite::Callbacks::Abort\n      if message = ex.message\n        Log.error { \"Destroy Exception: #{message}\" }\n        errors << Granite::Error.new(:base, message)\n      end\n      return false\n    end\n    true\n  end\n\n  # Same as `#destroy`, but raises `Granite::RecordNotDestroyed` if unsuccessful.\n  def destroy!\n    destroy || raise Granite::RecordNotDestroyed.new(self.class.name, self)\n  end\n\n  # Updates the *updated_at* field to the current time, without saving other fields.\n  #\n  # Raises error if record hasn't been saved to the database yet.\n  def touch(*fields) : Bool\n    raise \"Cannot touch on a new record object\" unless persisted?\n    {% begin %}\n      fields.each do |field|\n        case field.to_s\n          {% for time_field in @type.instance_vars.select { |ivar| ivar.type == Time? } %}\n            when {{time_field.stringify}} then @{{time_field.id}} = Time.local(Granite.settings.default_timezone).at_beginning_of_second\n          {% end %}\n        else\n          if {{@type.instance_vars.map(&.name.stringify)}}.includes? field.to_s\n            raise \"{{@type.name}}.#{field} cannot be touched.  It is not of type `Time`.\"\n          else\n            raise \"Field '#{field}' does not exist on type '{{@type.name}}'.\"\n          end\n        end\n      end\n    {% end %}\n    set_timestamps mode: :update\n    save\n  end\nend\n"
  },
  {
    "path": "src/granite/type.cr",
    "content": "module Granite::Type\n  extend self\n\n  # :nodoc:\n  PRIMITIVES = {\n    Int8    => \".read\",\n    Int16   => \".read\",\n    Int32   => \".read\",\n    Int64   => \".read\",\n    UInt8   => \".read\",\n    UInt16  => \".read\",\n    UInt32  => \".read\",\n    UInt64  => \".read\",\n    Float32 => \".read\",\n    Float64 => \".read\",\n    Bool    => \".read\",\n    String  => \".read\",\n  }\n\n  # :nodoc:\n  NUMERIC_TYPES = {\n    Int8    => \".to_i8\",\n    Int16   => \".to_i16\",\n    Int32   => \".to_i\",\n    Int64   => \".to_i64\",\n    UInt8   => \".to_u8\",\n    UInt16  => \".to_u16\",\n    UInt32  => \".to_u32\",\n    UInt64  => \".to_u64\",\n    Float32 => \".to_f32\",\n    Float64 => \".to_f\",\n  }\n\n  {% for type, method in PRIMITIVES %}\n    # Converts a `DB::ResultSet` to `{{type}}`.\n    def from_rs(result : DB::ResultSet, t : {{type}}.class) : {{type}}\n      result{{method.id}} {{type}}\n    end\n\n    # Converts a `DB::ResultSet` to `{{type}}?`.\n    def from_rs(result : DB::ResultSet, t : {{type}}?.class) : {{type}}?\n      result{{method.id}} {{type}}?\n    end\n\n    # Converts an `DB::ResultSet` to `Array({{type}})`.\n    def from_rs(result : DB::ResultSet, t : Array({{type}}).class) : Array({{type}})\n      result{{method.id}} Array({{type}})\n    end\n\n    # Converts an `DB::ResultSet` to `Array({{type}})?`.\n    def from_rs(result : DB::ResultSet, t : Array({{type}})?.class) : Array({{type}})?\n      result{{method.id}} Array({{type}})?\n    end\n  {% end %}\n\n  # Converts a `DB::ResultSet` to `Time`.\n  def from_rs(result : DB::ResultSet, t : Time.class) : Time\n    result.read(Time).in(Granite.settings.default_timezone)\n  end\n\n  # Converts a `DB::ResultSet` to `Time?`\n  def from_rs(result : DB::ResultSet, t : Time?.class) : Time?\n    result.read(Time?).try &.in(Granite.settings.default_timezone)\n  end\n\n  {% for type, method in NUMERIC_TYPES %}\n    # Converts a `String` to `{{type}}`.\n    def convert_type(value : String, t : {{type.id}}.class) : {{type.id}}\n      value{{method.id}}\n    end\n\n    # Converts a `String` to `{{type}}?`.\n    def convert_type(value : String, t : {{type.id}}?.class) : {{type.id}}?\n      value{{method.id}}\n    end\n  {% end %}\n\n  def convert_type(value, type)\n    value\n  end\n\n  def convert_type(value, type : Bool?.class) : Bool\n    [\"1\", \"yes\", \"true\", true, 1].includes?(value)\n  end\nend\n"
  },
  {
    "path": "src/granite/validation_helpers/blank.cr",
    "content": "module Granite::ValidationHelpers\n  macro validate_not_blank(field)\n    validate {{field}}, \"#{{{field}}} must not be blank\", Proc(self, Bool).new { |model| !model.{{field.id}}.to_s.blank? }\n  end\n\n  macro validate_is_blank(field)\n    validate {{field}}, \"#{{{field}}} must be blank\", Proc(self, Bool).new { |model| model.{{field.id}}.to_s.blank? }\n  end\nend\n"
  },
  {
    "path": "src/granite/validation_helpers/choice.cr",
    "content": "module Granite::ValidationHelpers\n  macro validate_is_valid_choice(field, choices)\n    validate {{field}}, \"#{{{field}}} has an invalid choice. Valid choices are: #{{{choices.join(',')}}}\", Proc(self, Bool).new { |model| {{choices}}.includes? model.{{field.id}} }\n  end\nend\n"
  },
  {
    "path": "src/granite/validation_helpers/exclusion.cr",
    "content": "module Granite::ValidationHelpers\n  macro validate_exclusion(field, excluded_values)\n    validate {{field}}, \"#{{{field.capitalize}}} got reserved values. Reserved values are #{{{excluded_values.join(',')}}}\", Proc(self, Bool).new { |model| !{{excluded_values}}.includes? model.{{field.id}}}\n  end\nend\n"
  },
  {
    "path": "src/granite/validation_helpers/inequality.cr",
    "content": "module Granite::ValidationHelpers\n  macro validate_greater_than(field, amount, or_equal_to = false)\n    validate {{field}}, \"#{{{field}}} must be greater than#{{{or_equal_to}} ? \" or equal to\" : \"\"} #{{{amount}}}\", Proc(self, Bool).new { |model| (model.{{field.id}}.not_nil! {% if or_equal_to %} >= {% else %} > {% end %} {{amount.id}}) }\n  end\n\n  macro validate_less_than(field, amount, or_equal_to = false)\n    validate {{field}}, \"#{{{field}}} must be less than#{{{or_equal_to}} ? \" or equal to\" : \"\"} #{{{amount}}}\", Proc(self, Bool).new { |model| (model.{{field.id}}.not_nil! {% if or_equal_to %} <= {% else %} < {% end %} {{amount.id}}) }\n  end\nend\n"
  },
  {
    "path": "src/granite/validation_helpers/length.cr",
    "content": "module Granite::ValidationHelpers\n  macro validate_min_length(field, length)\n    validate {{field}}, \"#{{{field}}} is too short. It must be at least #{{{length}}}\", Proc(self, Bool).new { |model| (model.{{field.id}}.not_nil!.size >= {{length.id}}) }\n  end\n\n  macro validate_max_length(field, length)\n    validate {{field}}, \"#{{{field}}} is too long. It must be at most #{{{length}}}\", Proc(self, Bool).new { |model| (model.{{field.id}}.not_nil!.size <= {{length.id}}) }\n  end\nend\n"
  },
  {
    "path": "src/granite/validation_helpers/nil.cr",
    "content": "module Granite::ValidationHelpers\n  macro validate_not_nil(field)\n    validate {{field}}, \"#{{{field}}} must not be nil\", Proc(self, Bool).new { |model| !model.{{field.id}}.nil? }\n  end\n\n  macro validate_is_nil(field)\n    validate {{field}}, \"#{{{field}}} must be nil\", Proc(self, Bool).new { |model| model.{{field.id}}.nil? }\n  end\nend\n"
  },
  {
    "path": "src/granite/validation_helpers/uniqueness.cr",
    "content": "module Granite::ValidationHelpers\n  macro validate_uniqueness(field)\n    validate {{field}}, \"#{{{field}}} should be unique\", -> (model: self) do\n      return true if model.{{field.id}}.nil?\n\n      instance = self.find_by({{field.id}}: model.{{field.id}})\n\n      !(instance && instance.id != model.id)\n    end\n  end\nend\n"
  },
  {
    "path": "src/granite/validators.cr",
    "content": "require \"./error\"\n\n# Analyze validation blocks and procs\n#\n# By example:\n# ```\n# validate :name, \"can't be blank\" do |user|\n#   !user.name.to_s.blank?\n# end\n#\n# validate :name, \"can't be blank\", ->(user : User) do\n#   !user.name.to_s.blank?\n# end\n#\n# name_required = ->(model : Granite::Base) { !model.name.to_s.blank? }\n# validate :name, \"can't be blank\", name_required\n# ```\nmodule Granite::Validators\n  @[JSON::Field(ignore: true)]\n  @[YAML::Field(ignore: true)]\n\n  # Returns all errors on the model.\n  getter errors = [] of Error\n\n  macro included\n    macro inherited\n      @@validators = Array({field: String, message: String, block: Proc(self, Bool)}).new\n\n      disable_granite_docs? def self.validate(message : String, &block : self -> Bool)\n        self.validate(:base, message, block)\n      end\n\n      disable_granite_docs? def self.validate(field : (Symbol | String), message : String, &block : self -> Bool)\n        self.validate(field, message, block)\n      end\n\n      disable_granite_docs? def self.validate(message : String, block : self -> Bool)\n        self.validate(:base, message, block)\n      end\n\n      disable_granite_docs? def self.validate(field : (Symbol | String), message : String, block : self -> Bool)\n        @@validators << {field: field.to_s, message: message, block: block}\n      end\n    end\n  end\n\n  # Runs all of `self`'s validators, returning `true` if they all pass, and `false`\n  # otherwise.\n  #\n  # If the validation fails, `#errors` will contain all the errors responsible for\n  # the failing.\n  def valid?\n    # Return false if any `ConversionError` were added\n    # when setting model properties\n    return false if errors.any? ConversionError\n\n    errors.clear\n\n    @@validators.each do |validator|\n      unless validator[:block].call(self)\n        errors << Error.new(validator[:field], validator[:message])\n      end\n    end\n\n    errors.empty?\n  end\nend\n"
  },
  {
    "path": "src/granite/version.cr",
    "content": "module Granite\n  VERSION = {{ `shards version #{__DIR__}`.strip.stringify }}\nend\n"
  },
  {
    "path": "src/granite.cr",
    "content": "require \"yaml\"\nrequire \"db\"\nrequire \"log\"\n\nmodule Granite\n  Log = ::Log.for(\"granite\")\n\n  TIME_ZONE       = \"UTC\"\n  DATETIME_FORMAT = \"%F %X%z\"\n\n  alias ModelArgs = Hash(Symbol | String, Granite::Columns::Type)\n\n  annotation Relationship; end\n  annotation Column; end\n  annotation Table; end\nend\n\nrequire \"./granite/base\"\n"
  }
]