Loading the parity record
Fetching the measured run — the pinned upstream oracle, every symbol and every case.
Fetching the measured run — the pinned upstream oracle, every symbol and every case.
Parity / Migrate
Every number on this page was produced by running both implementations over the same cases: the real activerecord package pinned at 8.0.2 answers first, and its answer is the expectation the Go port is held to. Nothing is a hand-written expectation, so a new upstream release re-scores the port on its own. See Migrate for the port's own documentation. Source: github.com/malcolmston/migrate.
Not a generic diagram: every node below names an artefact of this harness — the pinned package it installed, the runner files it started, the case files it streamed, and the counts it wrote out.
| Case group | Cases | Match | Mismatch | Group parity |
|---|---|---|---|---|
| ddl | 45 | 22 | 23 | 48.9% |
| types | 24 | 12 | 12 | 50.0% |
| mechanics | 17 | 10 | 7 | 58.8% |
Every exported symbol of the upstream package, and what the port offers for it. The upstream list is derived mechanically, never from a README: parity/migrate/ruby $ bundle exec ruby -e '<the following>'
require "active_record"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: "/tmp/x.sqlite3")
c = ActiveRecord::Base.lease_connection
puts ActiveRecord::ConnectionAdapters::SchemaStatements.public_instance_methods(false).sort.inspect
puts ActiveRecord::ConnectionAdapters::SQLite3Adapter.public_instance_methods(false).sort.inspect
puts c.native_database_types.keys.sort.inspect
puts ActiveRecord::ConnectionAdapters::TableDefinition.public_instance_methods(false).sort.inspect
puts ActiveRecord::MigrationContext.public_i…. A symbol with no case is untested, never a match.
| Upstream symbol | Go symbol | Status | Cases | Note |
|---|---|---|---|---|
| add_belongs_to (alias of add_reference) | Schema.AddReference | differs | add-reference-plain | see add_reference |
| add_check_constraint | Schema.AddCheckConstraint | differs | add-check-constraint | AR's SQLite3 adapter emits invalid ALTER TABLE … ADD CHECK and fails; the port emits ALTER TABLE … ADD CONSTRAINT which SQLite also rejects — but the *port* succeeded here because SQLite accepted its spelling, producing a constraint AR never made, named chk_users_login_count____0 |
| add_column | Schema.AddColumn, AlterTable.* | differs | add-column-text, add-column-not-null-default, add-column-decimal | abstract types only exist for string/text/integer/bigint/boolean/timestamp on AlterTable; anything else needs a raw SQL type |
| add_columns | — | missing | — | no plural form |
| add_foreign_key | Schema.AddForeignKey | differs | add-foreign-key, add-foreign-key-on-delete-cascade, remove-foreign-key | AR rebuilds the table; the port emits ALTER TABLE … ADD CONSTRAINT, which SQLite rejects — the FK is silently never created |
| add_index | Schema.AddIndex | match | add-index-single, add-index-unique, add-index-composite, add-index-named, add-index-partial, add-index-expression | including partial and expression indexes and the index_<t>_on_<c> naming convention |
| add_reference | Schema.AddReference | differs | add-reference-plain, add-reference-index-and-fk, add-reference-polymorphic-index | AR indexes the reference by default and uses integer; the port uses BIGINT, adds no index unless asked, cannot add the FK on SQLite, and names the polymorphic index differently |
| add_timestamps | Schema.AddTimestamps | differs | add-timestamps, change-table-timestamps-and-unique-index | AR datetime(6); the port TIMESTAMP here but DATETIME from Table.Timestamps — inconsistent with itself |
| change_column | Schema.ChangeColumn | differs | change-column-type | AR rebuilds the table; the port emits ALTER COLUMN … TYPE, which SQLite rejects |
| change_column_comment | Schema.SetColumnComment | untested | — | SQLite has no COMMENT ON |
| change_column_default | Schema.ChangeColumnDefault, Schema.DropColumnDefault | differs | change-column-default, drop-column-default | AR rebuilds the table; the port emits ALTER COLUMN … SET/DROP DEFAULT, which SQLite rejects |
| change_column_null | Schema.ChangeColumnNull | differs | change-column-null-false | AR rebuilds the table; the port emits ALTER COLUMN … SET NOT NULL, which SQLite rejects |
| change_table | Schema.ChangeTable + AlterTable | differs | change-table-bulk, change-table-timestamps-and-unique-index | the block itself matches; only the timestamp type diverges |
| change_table_comment | Schema.SetTableComment | untested | — | SQLite has no COMMENT ON |
| create_join_table | Schema.CreateJoinTable | differs | create-join-table | the port's naive de-pluralisation yields assemblie_id where AR yields assembly_id, and BIGINT where AR uses integer |
| create_table | Schema.CreateTable | differs | table-default-primary-key, table-if-not-exists, table-without-id, table-named-primary-key, table-check-constraint-inline | default and id: false shapes; the port has no primary_key: option and no in-table check constraint |
| drop_join_table | Schema.DropJoinTable | match | drop-join-table | |
| drop_table | Schema.DropTable, Schema.DropTableIfExists | match | drop-table | |
| remove_belongs_to (alias) | Schema.RemoveReference | match | remove-reference | |
| remove_check_constraint | Schema.RemoveCheckConstraint | match | remove-check-constraint | end state agrees |
| remove_column | Schema.DropColumn | match | remove-column | |
| remove_columns | — | missing | — | no plural form |
| remove_constraint | — | missing | — | no generic constraint drop |
| remove_foreign_key | Schema.RemoveForeignKey | differs | remove-foreign-key | unusable on SQLite for the same reason as add_foreign_key |
| remove_index | Schema.DropIndex | match | remove-index-by-columns | the port takes a name only; the harness derives the conventional name |
| remove_reference | Schema.RemoveReference | match | remove-reference | |
| remove_timestamps | Schema.RemoveTimestamps | match | remove-timestamps | |
| rename_column | Schema.RenameColumn | match | rename-column, sequence-of-changes | |
| rename_index | Schema.RenameIndex | differs | rename-index | AR drops and recreates; the port emits ALTER INDEX, which SQLite has no syntax for |
| rename_table | Schema.RenameTable | match | rename-table | |
| truncate (DatabaseStatements) | Schema.TruncateTable | differs | truncate-table | AR issues DELETE FROM; the port emits TRUNCATE TABLE, which SQLite lacks |
| tables, table_exists?, table_comment, table_options, columns, column_exists?, primary_key, indexes, index_exists?, index_name_exists?, foreign_keys, foreign_key_exists?, check_constraints, check_constraint_exists?, views, view_exists?, data_sources, data_source_exists?, native_database_types, max_index_name_size, assume_migrated_upto_version (21) | — | missing | — | no introspection API in the port at all; the harness reads sqlite_master/pragmas itself |
| add_index_options, build_add_column_definition, build_change_column_default_definition, build_create_index_definition, build_create_join_table_definition, build_create_table_definition, check_constraint_options, columns_for_distinct, distinct_relation_for_primary_key, foreign_key_column_for, foreign_key_options, index_algorithm, internal_string_options_for_primary_key, options_include_default?, quoted_columns_for_index, schema_creation, table_alias_for, use_foreign_keys?, valid_column_definition_options, valid_primary_key_options, valid_table_definition_options (21) | — | missing | — | builder plumbing; nothing a port is expected to reproduce, listed for completeness |
| create_schema_dumper, dump_schema_information | SchemaDump, SchemaDump.String | untested | — | the port's dump is a hand-recorded script, not derived from a live schema |
| bulk_change_table, update_table_definition | Schema.ChangeTable | untested | — | reached indirectly by the change_table cases |
| index_name | (implicit in Schema.AddIndex) | untested | — | naming is asserted through add_index instead |
| type_to_sql | Dialect.columnType (unexported) | untested | — | not reachable from the port's public API |
| primary_key (implicit id) | Schema.CreateTable auto id | differs | table-without-id, table-named-primary-key | AR: integer PRIMARY KEY AUTOINCREMENT NOT NULL; the port omits NOT NULL, which is harmless for an INTEGER rowid alias but loses the constraint for a TEXT primary key |
| string | Table.String | differs | type-string, type-string-limit, type-string-default | AR emits an unlimited varchar; the port imposes VARCHAR(255) the caller never asked for. limit: matches |
| text | Table.Text | match | type-text | |
| integer | Table.Integer | match | type-integer | |
| bigint | Table.BigInteger | match | type-bigint | |
| float | Table.Float | differs | type-float | AR float, port REAL |
| decimal | Table.Decimal | match | type-decimal | DECIMAL(10,2) on both |
| numeric (alias of decimal) | Table.Decimal | untested | — | alias not separately exercised |
| datetime | Table.Timestamp | differs | type-datetime, type-datetime-precision, type-datetime-default-raw | AR defaults to datetime(6); the port emits bare DATETIME and **silently drops Precision** on SQLite |
| timestamp (alias of datetime) | Table.Timestamps, AlterTable.Timestamps | differs | type-timestamps, add-timestamps | as above, and the two port spellings disagree with each other (DATETIME vs TIMESTAMP) |
| time | Table.Time | match | type-time | |
| date | Table.Date | match | type-date | |
| binary | Table.Binary | match | type-binary | |
| blob (alias of binary) | Table.Binary | untested | — | alias not separately exercised |
| boolean | Table.Boolean | match | type-boolean | |
| json | Table.JSON | match | type-json | |
| jsonb (not native on SQLite) | Table.JSONB | differs | type-jsonb | AR passes jsonb through; the port folds it to JSON |
| uuid (not native on SQLite) | Table.UUID | differs | type-uuid | AR passes uuid through; the port emits VARCHAR(36) |
| enum (PostgreSQL/MySQL only) | Table.Enum | differs | type-enum | AR refuses** (ArgumentError: Unknown key: :values); the port silently produces VARCHAR(255) with no CHECK, so the enumeration is lost with no diagnostic |
| array: true (PostgreSQL only) | Array() | differs | type-array-of-string | AR refuses** (ArgumentError: Unknown key: :array); the port silently produces JSON on SQLite (and JSON on MySQL, base[] on PostgreSQL, base ARRAY on ANSI) with no diagnostic |
| virtual / t.as (generated columns) | — | missing | — | not ported |
| limit: | Limit | match | type-string-limit | |
| precision: | Precision | differs | type-datetime-precision | dropped by the port's SQLite dialect |
| scale: | Scale | match | type-decimal | |
| null: | NotNull | match | type-not-null-and-default | |
| default: | Default, DefaultRaw | differs | type-boolean-default-false, type-string-default, type-datetime-default-raw | booleans: AR stores 0/1, the port stores FALSE/TRUE — SQLite has no boolean literal, so the port's default is the string FALSE |
| primary_key: (on a column) | PrimaryKey | differs | table-without-id | the port does not imply NOT NULL |
| collation: | — | missing | — | not ported |
| comment: | SetColumnComment (statement only) | missing | — | no per-column comment inside create_table |
| if_not_exists: | IfNotExists | match | table-if-not-exists | |
| migrate | Migrator.Migrate, Migrator.MigrateTo | match | up-all-then-version, up-idempotent, rollback-all, migrate-to-middle-then-forward | |
| up | Migrator.MigrateTo (upward) | match | migrate-to-middle-then-forward | |
| down | Migrator.MigrateTo (downward), Migrator.Down | match | rollback-all | |
| forward | Migrator.Up(n) | differs | up-one-step-at-a-time | AR counts steps from the *current migration's index*, so forward(1) from version 0 applies **two** migrations; the port's Up(1) applies exactly one pending migration |
| rollback | Migrator.Rollback | differs | rollback-one, rollback-empty-down, rollback-irreversible-no-down | a migration with no down method: AR silently un-records the version and leaves the table behind; the port refuses with ErrMissingMigration and keeps the version |
| current_version | Migrator.Version | match | up-all-then-version | |
| get_all_versions | Migrator.Status (applied set) | match | every mechanics case | |
| migrations | Migrator.Migrations | match | every mechanics case | |
| migrations_status | Migrator.Status | match | status-across-a-partial-migration | the {version, applied} projection agrees; **neither** side carries a failed/dirty flag |
| schema_migration | Migrator.EnsureSchemaTable table | differs | bookkeeping-table-shape | AR: schema_migrations(version VARCHAR PRIMARY KEY); the port: schema_migrations(version BIGINT PRIMARY KEY, applied_at TIMESTAMP NOT NULL) |
| internal_metadata | — | missing | — | the port has no metadata table, so no environment or schema-format record |
| current_environment | — | missing | — | |
| last_stored_environment | — | missing | — | |
| protected_environment? | — | missing | — | no production guard |
| needs_migration? | — | missing | — | derivable from Status, not exposed |
| pending_migration_versions | — | missing | — | derivable from Status, not exposed |
| open | — | missing | — | |
| run | — | missing | — | no "run exactly this one version" entry point |
| migrations_paths | LoadDir, LoadFS | untested | — | the file loader is not exercised; cases register migrations programmatically |
| migrate | Migrator.Migrate | match | up-all-then-version | |
| migrations | Migrator.Migrations | match | mechanics | |
| migrated | Migrator.Status | match | mechanics | |
| load_migrated | Migrator.Status | match | mechanics | |
| current | — | missing | — | |
| current_migration | — | missing | — | |
| pending_migrations | — | missing | — | |
| runnable | — | missing | — | |
| up | Migration.Up / Migration.UpSQL | match | every mechanics case | but see the raw-SQL findings below |
| down | Migration.Down / Migration.DownSQL | match | rollback-one, rollback-empty-down | |
| change | Change, ChangeWith, ChangeRecorder | untested | — | the harness drives explicit up/down so both sides are compared on the same footing |
| revert | ChangeRecorder inverse | untested | — | |
| ActiveRecord::IrreversibleMigration | ErrIrreversibleMigration | untested | — | raised only by change-style migrations, which are untested |
| disable_ddl_transaction! | — | missing | — | the port always wraps a migration in a transaction, with no opt-out |
Every case the harness streamed to both runners, with the exact upstream symbol and Go symbol it exercised. A deliberate, documented difference is a deviation and is counted apart from a mismatch.
| Case | Group | Upstream symbol | Go symbol | Status | Note |
|---|---|---|---|---|---|
| table-default-primary-key | ddl | create_table | Schema.CreateTable | match | |
| table-without-id | ddl | create_table id: false | Schema.CreateTable + WithoutID | mismatch | |
| table-named-primary-key | ddl | create_table primary_key: | — | mismatch | |
| table-if-not-exists | ddl | create_table if_not_exists: true | Schema.CreateTable + IfNotExists | match | |
| table-check-constraint-inline | ddl | t.check_constraint | — | mismatch | |
| drop-table | ddl | drop_table | Schema.DropTable | match | |
| rename-table | ddl | rename_table | Schema.RenameTable | match | |
| add-column-text | ddl | add_column | Schema.ChangeTable + AlterTable.Text | match | |
| add-column-not-null-default | ddl | add_column null: false, default: | AlterTable.Integer + NotNull + Default | match | |
| add-column-decimal | ddl | add_column :decimal | Schema.AddColumn (raw type only) | mismatch | AlterTable has no abstract decimal; the port needs a raw SQL type spelling here |
| remove-column | ddl | remove_column | Schema.DropColumn | match | |
| rename-column | ddl | rename_column | Schema.RenameColumn | match | |
| change-column-type | ddl | change_column | Schema.ChangeColumn | mismatch | SQLite cannot ALTER COLUMN ... TYPE; ActiveRecord rebuilds the table instead |
| change-column-null-false | ddl | change_column_null | Schema.ChangeColumnNull | match | SQLite cannot ALTER COLUMN ... SET NOT NULL; ActiveRecord rebuilds the table |
| change-column-default | ddl | change_column_default | Schema.ChangeColumnDefault | mismatch | SQLite cannot ALTER COLUMN ... SET DEFAULT; ActiveRecord rebuilds the table |
| drop-column-default | ddl | change_column_default(nil) | Schema.DropColumnDefault | mismatch | |
| add-index-single | ddl | add_index | Schema.AddIndex | match | |
| add-index-unique | ddl | add_index unique: true | Schema.AddIndex + UniqueIndex | match | |
| add-index-composite | ddl | add_index [a, b] | Schema.AddIndex (multi-column) | match | |
| add-index-named | ddl | add_index name: | Schema.AddIndex + IndexName | match | |
| add-index-partial | ddl | add_index where: | Schema.AddIndex + Where | match | |
| add-index-expression | ddl | add_index "lower(email)" | Schema.AddIndex (expression column) | match | |
| remove-index-by-columns | ddl | remove_index | Schema.DropIndex (name only) | match | |
| rename-index | ddl | rename_index | Schema.RenameIndex | mismatch | SQLite has no ALTER INDEX; ActiveRecord drops and recreates |
| add-timestamps | ddl | add_timestamps | Schema.AddTimestamps | mismatch | |
| remove-timestamps | ddl | remove_timestamps | Schema.RemoveTimestamps | match | |
| table-references-plain | ddl | t.references | Table.References | mismatch | |
| table-references-foreign-key | ddl | t.references foreign_key: true | Table.References + WithForeignKey | mismatch | |
| table-references-polymorphic | ddl | t.references polymorphic: true | Table.References + Polymorphic | mismatch | |
| add-reference-plain | ddl | add_reference | Schema.AddReference | mismatch | |
| add-reference-index-and-fk | ddl | add_reference index: true, foreign_key: true | Schema.AddReference + ReferenceIndex + WithForeignKey | mismatch | |
| add-reference-polymorphic-index | ddl | add_reference polymorphic: true, index: true | Schema.AddReference + Polymorphic + ReferenceIndex | mismatch | |
| remove-reference | ddl | remove_reference | Schema.RemoveReference | match | |
| add-foreign-key | ddl | add_foreign_key | Schema.AddForeignKey | mismatch | SQLite cannot ALTER TABLE ADD CONSTRAINT; ActiveRecord rebuilds the table |
| add-foreign-key-on-delete-cascade | ddl | add_foreign_key on_delete: :cascade | Schema.AddForeignKey + OnDelete(Cascade) | mismatch | |
| remove-foreign-key | ddl | remove_foreign_key | Schema.RemoveForeignKey | mismatch | |
| add-check-constraint | ddl | add_check_constraint | Schema.AddCheckConstraint | mismatch | the port's generated constraint name is mangled (chk_users_login_count____0); recorded raw in the diagnostics |
| remove-check-constraint | ddl | remove_check_constraint | Schema.RemoveCheckConstraint | match | |
| add-unique-constraint | ddl | add_unique_constraint | Schema.AddUniqueConstraint | match | ActiveRecord's SQLite3 adapter has no add_unique_constraint at all; the port emits ALTER TABLE ADD CONSTRAINT which SQLite rejects |
| create-join-table | ddl | create_join_table | Schema.CreateJoinTable | mismatch | |
| drop-join-table | ddl | drop_join_table | Schema.DropJoinTable | match | |
| truncate-table | ddl | truncate | Schema.TruncateTable | mismatch | SQLite has no TRUNCATE; ActiveRecord issues DELETE FROM |
| change-table-bulk | ddl | change_table | Schema.ChangeTable + AlterTable | match | the change_table bulk-alter block: add, rename, index and drop in one go |
| change-table-timestamps-and-unique-index | ddl | change_table (t.timestamps, t.index unique) | AlterTable.Timestamps + AlterTable.Index + UniqueIndex | mismatch | |
| sequence-of-changes | ddl | create_table + add_column + add_index + rename_column | Schema.CreateTable + ChangeTable + AddIndex + RenameColumn | mismatch | a realistic multi-step schema definition exercised end to end |
| up-all-then-version | mechanics | MigrationContext#migrate | Migrator.Migrate | match | |
| up-idempotent | mechanics | MigrationContext#migrate (twice) | Migrator.Migrate (twice) | match | running up twice must apply nothing the second time |
| up-one-step-at-a-time | mechanics | MigrationContext#forward(1) | Migrator.Up(ctx, 1) | mismatch | |
| rollback-one | mechanics | MigrationContext#rollback(1) | Migrator.Rollback(ctx, 1) | match | |
| rollback-then-reapply | mechanics | MigrationContext#rollback + forward | Migrator.Redo | match | |
| rollback-all | mechanics | MigrationContext#migrate(0) | Migrator.MigrateTo(ctx, 0) | match | |
| migrate-to-middle-then-forward | mechanics | MigrationContext#migrate(target) | Migrator.MigrateTo | match | |
| rollback-irreversible-no-down | mechanics | IrreversibleMigration | ErrMissingMigration | mismatch | no down direction at all: both sides must refuse |
| rollback-empty-down | mechanics | def down; end | Migration.Down (empty) | match | an explicitly empty down is a legitimate no-op rollback that should still un-record the version |
| fail-midway-second-op | mechanics | Migrator#migrate (transactional DDL) | Migrator.Migrate (per-migration transaction) | match | the second op of the second migration references a missing table; check the schema and the version table are left consistent |
| fail-midway-then-rollback-and-retry | mechanics | Migrator#migrate then rollback | Migrator.Migrate then Rollback | match | after a failed migration, roll back what did apply and try again: is any dirty state left behind? |
| raw-sql-two-statements | mechanics | execute (multi-statement) | Migration.UpSQL (splitStatements) | mismatch | the raw-SQL migration facility fed a two-statement script |
| raw-sql-semicolon-in-string-literal | mechanics | execute (semicolon inside a literal) | Migration.UpSQL (splitStatements) | mismatch | the port splits raw SQL on ';' with no string-literal awareness |
| raw-sql-trigger-body | mechanics | execute (CREATE TRIGGER ... BEGIN ...; END) | Migration.UpSQL (splitStatements) | mismatch | the port splits a trigger body on its internal semicolons |
| raw-sql-failing-second-statement | mechanics | execute (second statement invalid) | Migration.UpSQL (second statement invalid) | mismatch | the second statement of a raw script fails: is the first rolled back and the version left unrecorded? |
| status-across-a-partial-migration | mechanics | MigrationContext#migrations_status | Migrator.Status | match | the status projection {version, applied} after a partial run; neither side reports a failed/dirty state |
| bookkeeping-table-shape | mechanics | SchemaMigration (schema_migrations) | Migrator.EnsureSchemaTable (schema_migrations) | mismatch | the only case that compares the bookkeeping table itself: ActiveRecord tracks (version) as a string key, the port tracks (version BIGINT, applied_at TIMESTAMP) and has no dirty/failed column on either side |
| type-string | types | SchemaStatements#create_table + t.string | Schema.CreateTable + Table.String | mismatch | |
| type-string-limit | types | t.string limit: | Table.String + Limit | match | |
| type-text | types | t.text | Table.Text | match | |
| type-integer | types | t.integer | Table.Integer | match | |
| type-bigint | types | t.bigint | Table.BigInteger | match | |
| type-float | types | t.float | Table.Float | mismatch | |
| type-decimal | types | t.decimal precision: scale: | Table.Decimal | match | |
| type-boolean | types | t.boolean | Table.Boolean | match | |
| type-date | types | t.date | Table.Date | match | |
| type-time | types | t.time | Table.Time | match | |
| type-datetime | types | t.datetime | Table.Timestamp | mismatch | |
| type-datetime-precision | types | t.datetime precision: | Table.Timestamp + Precision | mismatch | |
| type-binary | types | t.binary | Table.Binary | match | |
| type-json | types | t.json | Table.JSON | match | |
| type-jsonb | types | t.column :c, :jsonb | Table.JSONB | mismatch | |
| type-uuid | types | t.column :c, :uuid | Table.UUID | mismatch | |
| type-enum | types | t.column :c, :enum, values: | Table.Enum | mismatch | port silently degrades an Enum to VARCHAR(255) with no CHECK; ActiveRecord rejects enum values on SQLite |
| type-array-of-string | types | t.string :c, array: true | Table.String + Array | mismatch | port maps Array() to three different things by dialect with no diagnostic; ActiveRecord rejects array: on SQLite |
| type-timestamps | types | t.timestamps | Table.Timestamps | mismatch | |
| type-not-null-and-default | types | t.integer null: false, default: | Table.Integer + NotNull + Default | match | |
| type-boolean-default-false | types | t.boolean default: false | Table.Boolean + Default(false) | mismatch | |
| type-string-default | types | t.string default: | Table.String + Default("anon") | mismatch | |
| type-datetime-default-raw | types | t.datetime default: -> { "CURRENT_TIMESTAMP" } | Table.Timestamp + DefaultRaw | mismatch | |
| type-inline-unique-column | types | t.text + add_index unique | Table.Text + AddIndex + UniqueIndex | match | uses text so the varchar-limit difference cannot pollute the unique-index comparison |