SchemaPlus::Core creates an internal extension API to ActiveRecord. The idea is that:
-
SchemaPlus::Core does the monkey-patching so clients don't have to know too much about the internal of ActiveRecord.
-
SchemaPlus::Core's extension API is consistent across the various connection adapters, so clients don't have to figure out how to extend each connection adapter independently.
-
SchemaPlus::Core's extension API intends to remain reasonably stable even as ActiveRecord changes.
By itself, SchemaPlus::Core does not change any behavior or add any external features to ActiveRecord. It just makes the API available to clients.
SchemaPlus::Core is a client of schema_monkey, using modware to define middleware callback stacks.
SchemaPlus::Core is tested on:
- ruby 2.5 with activerecord 5.2, using postgresql:9.6, mysql2 or sqlite3
- ruby 2.5 with activerecord 6.0, using postgresql:9.6, mysql2 or sqlite3
- ruby 2.5 with activerecord 6.1, using postgresql:9.6, mysql2 or sqlite3
- ruby 2.7 with activerecord 5.2, using postgresql:9.6, mysql2 or sqlite3
- ruby 2.7 with activerecord 6.0, using postgresql:9.6, mysql2 or sqlite3
- ruby 2.7 with activerecord 6.1, using postgresql:9.6, mysql2 or sqlite3
- ruby 2.7 with activerecord 7.0, using postgresql:9.6, mysql2 or sqlite3
- ruby 3.0 with activerecord 6.0, using postgresql:9.6, mysql2 or sqlite3
- ruby 3.0 with activerecord 6.1, using postgresql:9.6, mysql2 or sqlite3
- ruby 3.0 with activerecord 7.0, using postgresql:9.6, mysql2 or sqlite3
- ruby 3.1 with activerecord 6.0, using postgresql:9.6, mysql2 or sqlite3
- ruby 3.1 with activerecord 6.1, using postgresql:9.6, mysql2 or sqlite3
- ruby 3.1 with activerecord 7.0, using postgresql:9.6, mysql2 or sqlite3
SchemaPlus::Core::Schema::Tablesmiddleware was replaced bySchemaPlus::Core::DataSources. Parameters have also changed.SchemaPlus::Core::Dumper::Indexesmiddleware was removed; insteadSchemaPlus::Core::Dumper::Tablesets both columns and indexes on env.table.
As usual:
gem "schema_plus_core" # in a Gemfile
gem.add_dependency "schema_plus_core" # in a .gemspecThe API is in the form of a collection of modware middleware callback stacks. A client of the API uses schema_monkey to insert middleware modules into the stacks. As per schema_monkey, the typical module structure looks like:
require "schema_plus/core"
module MyClient
module Middleware
#
# Middleware modules to insert in SchemaPlus::Core API stacks
#
end
module ActiveRecord
#
# direct ActiveRecord enhancements, should your client need any.
#
end
end
SchemaMonkey.register MyClientFor example, a client could use the Migration::Index stack to automatically make an index unique if any column starts with 'u':
require "schema_plus/core"
module AutoUniquify
module Middleware
module Migration
module Index
def before(env)
env.options[:unique] = true if env.column_names.grep(/^u/).any?
end
end
end
end
end
SchemaMonkey.register AutoUniquifyIdeally most clients will not need to define direct ActiveRecord enhancements, other than perhaps to create new methods on public classes. If you have a client that needs more complex monkey-patching, that could be a sign that SchemaPlus::Core's API is missing some useful functionality -- consider submitting a PR to SchemaPlus::Core add it!
For organizational clarity, the SchemaPlus::Core stacks are grouped into modules based on broad categories. In the Env field tables below, Initialized
Stacks for general operations queries pertaining to the entire database schema:
-
Schema::DefineWrapper around the
ActiveRecord::Schema.definemethod loads a dumped schema file (schema.rb).Env Field Description Initial value :infoSchema information hash args :blockThe proc containing the schema definition statements args The base implementation calls the block to define the schema.
-
Schema::IndexesWrapper around the
connection.indexes(table_name)method. Env contains:Env Field Description Initial value :index_definitionsThe result of the lookup []:connectionThe current ActiveRecord connection context :table_nameThe name of the table to query arg The base implementation appends its results to
env.index_definitions -
Schema::DataSourcesWrapper around the
connection.data_sources()method. Env contains:Env Field Description Initialized :data_sourcesThe result of the lookup []:connectionThe current ActiveRecord connection context The base implementation appends its results to
env.tables
Stacks for class methods on ActiveRecord models.
-
Model::ColumnsWrapper around the
Model.columnsqueryEnv Field | Description | Initialized--- | --- | ---
:columns| The resulting Column objects |[]:model| The model Class being queried | contextThe base implementation appends its results to
env.columns -
Model::ResetColumnInformationWrapper around the
Model.reset_column_informationmethodEnv Field | Description | Initialized --- | --- | --- `:model` | The model Class being reset | *context*The base implementation performs the reset.
-
Model::Association::DeclarationWrapper around the
Model.has_many,Model.has_and_belongs_to_many,Model.has_one, andModel.belongs_tomethodsEnv Field | Description | Initialized --- | --- | --- `:model` | The model Class being defined | *context* `:name` | The name of the association being defined. | *arg* `:scope` | The scope lambda associated with the association | *arg* `:options` | Options associated with the association. | *arg* `:extension` | Extensions to the association to be made. | *arg*The base implementation creates the association.
Stacks for operations that change the schema. In some cases the operation immediately modifies the database schema, in others the operation defines ActiveRecord objects (e.g., column definitions in a create_table definition) and the actual modification of the database schema will happen some time later.
-
Migration::ColumnCallback stack for various ways to define or modify a column.
Env Field Description Initialized :callerThe ActiveRecord instance responsible for performing the action context :operationOne of :add,:change,:define,:recordcontext :table_nameThe name of the table arg :column_nameThe name of the column arg :typeThe ActiveRecord column type ( :integer,:datetime, etc.)arg :implements_referenceThis implements a migration.add_reference,t.referencesort.belongs_to[See below]context :optionsThe column options arg, default {}The base implementation performs the column operation. No value is returned.
Notes:
-
The
:operationfield has the following meanings::add- The column will be added immediately (Migration#add_column):change- The column will be changed immediately (Migration#change_column):define- The column will be added to table definition, which will be emitted later:record- The column info will be added to a migration command recorder, for later playback in reverse byMigration#down
-
In the case of a table definition using
t.referencesort.belongs_to, the:typefield will be set to:referenceand the:column_namewill include the"_id"suffix -
ActiveRecord's base implementation handles
migration.add_reference,t.referencesandt.belongs_toby making nested calls tomigration.add_columnort.columnto create the resulting column (or two columns, for polymorphic references). SchemaPlus::Core invokes theMigration::Columnstack for both the outermigration.add_reference,t.referencesort.belongs_tocall, as well as for the nestedmigration.add_columnort.columncall; in the nested call,env.implements_referencewill be truthy. -
Sqlite3 implements
change_columnby a creating a new table. This will result in nested calls toadd_column, invoking theMigration::Columnstack for each; SchemaPlus::Core does not currently provide a way to distinguish those calls from explicit top-level calls.
-
-
Migration::CreateTableCreates a new table
Env Field Description Initialized :callerThe ActiveRecord instance responsible for creating the table context :table_nameThe name of the table arg :optionsCreate table options arg :blockProc containing table definition statements arg The base implementation creates the table, yielding a
table_definitioninstance to the block (if a block is given). -
Migration::DropTableDrops a table from the database
Env Field Description Initialized :connectionThe current ActiveRecord connection context :table_nameThe name of the table arg :optionsDrop table options arg The base implementation drops the table. No value is returned.
-
Migration::RenameTableRenames a table
Env Field Description Initialized :connectionThe current ActiveRecord connection context :table_nameThe existing name of the table arg :new_nameThe target name of the table arg The base implementation renames the table. No value is returned.
-
Migration::IndexCallback stack for various ways to define an index.
Env Field Description Initialized :callerThe ActiveRecord instance responsible for performing the action context :operation:addor:definecontext :table_nameThe name of the table arg :column_namesThe names of the columns arg :optionsThe index options arg, default {}The base implementation performs the index creation operation. No value is returned.
Notes:
-
The
:operationfield has the following meanings::add- The index will be added immediately (Migration#add_index):define- The index will be added to a table definition, which will be emitted later.
-
Stacks for internal operations that generate SQL.
-
Sql::ColumnOptionsCallback stack around generation of the SQL options for a column definition.
Env Field Description Initialized :sqlThe resulting SQL "":callerThe ActiveRecord::SchemaCreation instance context :connectionThe current ActiveRecord connection context :columnThe column definition object context :optionsThe column definition options context The base implementation appends the options SQL to
env.sql -
Sql::IndexComponentsCallback stack around generation of the SQL for an index definition.
Env Field Description Initialized :sqlThe resulting SQL components, in a struct with fields :name,:type,:columns,:options,:algorithm,:usingempty struct :connectionThe current ActiveRecord connection context :table_nameThe name of the table context :column_namesThe names of the columns context :optionsThe index options context The base implementation overwrites the contents of
env.sqlNotes:
- SQLite3 ignores the
:type,:algoritm, and:usingfields ofenv.sql
- SQLite3 ignores the
-
Sql::TableCallback stack around generation of the SQL for a table
Env Field Description Initialized :sqlThe resulting SQL components in a struct with fields :command,:name,:body,:options,:quotecharempty struct :callerThe ActiveRecord::SchemaCreation instance context :connectionThe current ActiveRecord connection context :table_definitionThe TableDefinition object for the table context The base implementation overwrites the contents of
env.sqlNotes:
env.sql.commandcontains the index creation command such asCREATE TABLEorCREATE TEMPORARY TABLEenv.sql.quotecharcontains the quote character ', ", or ` to wrapenv.sql.namein.
Stacks around low-level query execution
-
Query::ExecCallback stack wraps the emission of sql to the underlying dbms gem.
Env Field | Description | Initialized--- | --- | ---
:result| The result of the database query | unset:caller| The ActiveRecord::SchemaCreation instance | context:sql| The SQL string | context:binds| Values to substitute into the SQL string:query_name| Label sometimes used by ActiveRecord logging | arg
SchemaPlus::Core provides a state object and of callbacks to various phases of the schema dumping process. The dumping process fleshes out the state object-- nothing is actually written to the dump file until after the state is fleshed out.
-
Class SchemaPlus::Core::SchemaDumpAn instance of
SchemaPlus::Core::SchemaDumpgets passed to each of the callback stacks; the dump gets built up by fleshing out its contents.SchemaDumphas the following fields and methods:dump.initial = []- an array of strings containing statements to start the schema with, such asenable_extension 'hstore'dump.data = OpenStruct.new- a place for clients to store arbitrary data between phasesdump.tables = {}- a hash mapping table names to SchemaDump::Table objectsdump.final = []- an array of strings containing statements to end the schema with.dump.depends(table_name, [prerequisite_table_names])- call this method to ensure that the definition oftable_namewon't be output before its prerequisites.
-
Class SchemaPlus::Core::SchemaDump::TableEach table in the dump has its contents in a SchemaDump::Table object, with these fields:
table.name- the table nametable.pname- table as actually used in SQL (without any prefixes or suffixes)table.options- a string containing the options toMigration.create_tabletable.columns = []- an array of SchemaDump::Table::Column objectstable.indexes = []- an array of SchemaDump::Table::Index objectstable.statements- a collection of statements to include in the table definition; each is a string that should start with"t."table.trailer- a collection of migration statements to include immediately outside the table definition. Each is a stringtable.alt- In some cases, ActiveRecord is unable to dump a table in the form of a migrationcreate_tablestatement; in this casetable.pnamewill be nil, andtable.altwill contain the alternate string to dump instead. (E.g. if the table contains custom types, ActiveRecord will be unable to handle it and will just dump an error message as a comment.)
-
Class SchemaPlus::Core::SchemaDump::Table::ColumnEach column in a table has its contents in a SchemaDump::Table::Column object, with these fields and methods:
column.name- the column namecolumn.type- the column type (i.e., what comes after"t.")column.options- a hash containing the options for the columncolumn.comments- an array of comment strings for the column
-
Class SchemaPlus::Core::SchemaDump::Table::IndexEach index in a table has its contents in a SchemaDump::Table::Index object, with these fields and methods:
index.name- the index nameindex.columns- the columns that are in the indexindex.options- a hash containing the options for the column
-
Dumper::InitialCallback stack wraps the creation of initial statements for the dump.
Env Field Description Initialized :initialThe initial statements [] :dumpThe SchemaDump object context :dumperThe current ActiveRecord::SchemaDumper instance context :connectionThe current ActiveRecord connection context The base method appends initial statements to
env.initial. -
Dumper::TablesCallback stack wraps the dumping of all tables.
Env Field Description Initialized :dumpThe SchemaDump object context :dumperThe current ActiveRecord::SchemaDumper instance context :connectionThe current ActiveRecord connection context The base method iterates through all tables, dumping each.
-
Dumper::TableCallback stack wraps the dumping of each table
Env Field Description Initialized :tableA SchemaDump::Table object table.nameonly:dumpThe SchemaDump object context :dumperThe current ActiveRecord::SchemaDumper instance context :connectionThe current ActiveRecord connection context The base method iterates through all columns and indexes of the table, and overwrites the contents of
table,Notes:
- When the stack is called,
env.dump.tables[env.table.name]contains theenv.tableobject. - The base method sets both
env.table.columnsandenv.tables.indexes.
- When the stack is called,
- 3.1.0 Add AR 6.1 and 7.0, add Ruby 3.1
- 3.0.0 Drop AR < 5.2 and add AR 6.0 support. Drop Ruby < 2.5 and add Ruby 3.0 support.
- 2.2.3 Fix dumping complex expression based indexes in AR 5.x
- 2.2.2 Fixed dumping tables in postgresql in AR 5.2 when the PK is not a bigint.
- 2.2.1 Fixed expression index handling in AR5.x.
- 2.2.0 Added AR5.2 support. Thanks to @jeremyyap
- 2.1.1 Bug fix: Don't lose habtm options. Thanks to @iagopiimenta
- 2.1.0 Added AR5.1 support. Thanks to @iagopiimenta
- 2.0.1 Tighten up AR dependency. Thanks to @myabc.
- 2.0.0 Added AR5 support, removed AR4.2 support. Thanks to @boazy.
- 1.0.2 Missing require
- 1.0.1 Explicit gem dependencies
- 1.0.0 Clean up
SchemaDump::Table::ColumnandSchemaDump::Table::IndexAPI:#optionsis now a hash and#commentsis now an array; no longer haveadd_optionandadd_commentmethods. - 0.6.2 Bug fix: don't choke on INHERITANCE in table definition (#7). Thanks to @ADone.
- 0.6.1 Make sure to require pathname (#5)
- 0.6.0 Added
table.altto dumper; Bug fix: Don't crash when AR fails to dump a table. Thanks to @stenver for tracking it down - 0.5.1 Bug fix: Don't choke on a quoted newline in a
CREATE TABLEstatement (#3). Thanks to @mikeauclair - 0.5.0 Added
Migration::DropTable - 0.4.0 Added
implements_referencetoMigration::Columnstack env - 0.3.1 Pass along (undocumented) return values from association declarations (#2). Thanks to @lowjoel
- 0.3.0 Added
Model::Association::Declaration(#1). Thanks to @lowjoel. - 0.2.1 Added
Migration::CreateTableandSchema::Define; removed dependency on (defunct)schema_monkey_railsgem. [Oops, this should have been a minor version bump] - 0.2.0 Added
Migration::DropTable - 0.1.0 Initial release
Are you interested in contributing to SchemaPlus::Core? Thanks! Please follow the standard protocol: fork, feature branch, develop, push, and issue pull request.
Some things to know about to help you develop and test:
-
schema_dev: SchemaPlus::Core uses schema_dev to facilitate running rspec tests on the matrix of ruby, activerecord, and database versions that the gem supports, both locally and on github actions
To to run rspec locally on the full matrix, do:
$ schema_dev bundle install $ schema_dev rspecYou can also run on just one configuration at a time; For info, see
schema_dev --helpor the schema_dev README.The matrix of configurations is specified in
schema_dev.ymlin the project root.
- schema_monkey: SchemaPlus::Core is implemented as a schema_monkey client, using schema_monkey's convention-based protocols for extending ActiveRecord and using middleware stacks.