dameng-rails 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 31605a94e92ed817c32ec8f0e40382a994e4b0d3dbab66e4ea0deeb9559561bb
4
+ data.tar.gz: 0adae4275c6fb981fa04c620ff388bf748db5ea74aa39d5fc17eaee71ff0ac31
5
+ SHA512:
6
+ metadata.gz: a13c115aaab33f595051b3be0f964900983c8f48c11fd1aedc0d4270d8d95c9f3eba900b21325fe063622087a8462d0603d4cf3e9f6b53000893457a78367a95
7
+ data.tar.gz: f20958ce3fbbceed43683e2d4d206f001fa82a5b53f2a4e01d93aea47c2a4d8e3d645f305d21bab0837cc6f8f519787ebfe5e0737b7f93f670811321fa2878f4
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 qinyuanmao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # dm-rails
2
+
3
+ 让 Ruby on Rails 运行在达梦数据库(DM8)上。
4
+
5
+ Run Ruby on Rails on DM (Dameng) database — via a MySQL-protocol proxy, no Ruby driver needed.
6
+
7
+ ## 背景
8
+
9
+ 达梦没有 Ruby 驱动,也没有 ActiveRecord 适配器,Rails 无法直连。dm-rails 采用「协议桥接」路线:
10
+
11
+ ```
12
+ Rails (mysql2 适配器,零改动)
13
+ │ MySQL 协议
14
+
15
+ ShardingSphere-Proxy 5.5.0 + 本仓库达梦 SPI 适配包(proxy/ss-dm-dialect)
16
+ │ 达梦 JDBC
17
+
18
+ 达梦 DM8(MySQL 兼容模式)
19
+ ```
20
+
21
+ gem 负责 Ruby 侧:把 ActiveRecord 的 MySQL 自省语句(`SHOW FULL FIELDS` / `SHOW KEYS` /
22
+ `SHOW CREATE TABLE` / `information_schema`——代理与达梦均不支持)替换为查询达梦侧辅助视图。
23
+
24
+ 本方案在一套生产环境 Rails 7.1 IoT 平台上完整验证(含 migration、seed、动态查询、定时任务)。
25
+
26
+ ## 达梦实例要求(硬性)
27
+
28
+ 初始化参数(`dminit`):
29
+
30
+ | 参数 | 值 | 说明 |
31
+ |---|---|---|
32
+ | `CHARSET` | `1` (UTF-8) | 初始化级,装错重建 |
33
+ | `CASE_SENSITIVE` | `0`(不敏感) | **成败关键**:Rails 经代理发出的是反引号小写标识符 |
34
+ | `COMPATIBLE_MODE`(dm.ini) | `4` (MySQL) | 可改配置后重启生效 |
35
+
36
+ ## 安装
37
+
38
+ ### 1. Ruby 侧
39
+
40
+ ```ruby
41
+ # Gemfile
42
+ gem "dameng-rails" # rubygems.org(源码仓库为 github.com/qinyuanmao/dm-rails)
43
+ ```
44
+
45
+ 环境变量:
46
+
47
+ ```bash
48
+ DM_RAILS_ENABLED=1 # 启用补丁(不设则 gem 完全无副作用)
49
+ # DM_RAILS_VIEW=dm_rails_columns # 辅助视图名,默认即此
50
+ ```
51
+
52
+ `database.yml` 照常用 mysql2 适配器,host/port 指向代理(如 `127.0.0.1:3307`)。
53
+
54
+ ### 2. 达梦侧辅助视图
55
+
56
+ 用达梦客户端(disql 等)在业务 schema 下执行 [`sql/dm_rails_columns.sql`](sql/dm_rails_columns.sql)
57
+ (或 `bin/rails dm_rails:view_sql` 输出后拷去执行)。**不要经代理执行**(视图 DDL 是达梦方言)。
58
+
59
+ ### 3. 代理侧
60
+
61
+ 1. 构建 SPI 适配包:`cd proxy/ss-dm-dialect && mvn package`
62
+ 2. 把 `ss-dm-dialect-1.0.0.jar` 与达梦 JDBC 驱动(`DmJdbcDriver18`)放入
63
+ ShardingSphere-Proxy 的 `ext-lib/`
64
+ 3. 逻辑库配置见 [`proxy/conf/database-example.yaml`](proxy/conf/database-example.yaml),两个必踩点:
65
+ - URL 前缀用 `jdbc:dmwrap:`(包装驱动,修正 CLOB→LONGVARCHAR 类型码、列标签转小写、
66
+ `getObject` 的 Clob/Blob 对象转实际内容)
67
+ - 必须声明 `!SINGLE tables: ["*.*"]`,否则代理不做表发现
68
+
69
+ ## SPI 适配包做了什么
70
+
71
+ | SPI | 作用 |
72
+ |---|---|
73
+ | `DatabaseType`(trunk=MySQL) | 让代理认识 `jdbc:dm:` / `jdbc:dmwrap:` |
74
+ | `DialectMetaDataLoader` | 达梦无 information_schema,改查 `USER_TAB_COLUMNS`/`USER_CONSTRAINTS`,名称转小写 |
75
+ | `ConnectionPropertiesParser` | 解析达梦 URL 的 `schema` 参数(否则元数据无目标库) |
76
+ | `DialectDatabaseMetaData` | 反引号引用、NULL 排序对齐 MySQL |
77
+ | `java.sql.Driver`(dmwrap) | 动态代理包装真实驱动,修结果集类型码/标签/LOB 取值 |
78
+
79
+ ## 从 MySQL 迁移数据的语义注意事项
80
+
81
+ - 应用侧生成主键(雪花 ID 等)的表**不要**建成 IDENTITY 列(达梦禁止对 IDENTITY 显式赋值)
82
+ - MySQL TIMESTAMP 的「插入 NULL 自动变当前时间」语义达梦没有,可用 BEFORE INSERT 触发器模拟:
83
+ `:NEW.created_at := NVL(:NEW.created_at, CURRENT_TIMESTAMP);`
84
+ - 达梦索引名是 schema 级全局唯一(MySQL 是表级),迁移索引需加表名前缀
85
+ - `LENGTH_IN_CHAR=0`(字节语义)时 VARCHAR 长度按 MySQL 版 ×4
86
+
87
+ ## 已知限制
88
+
89
+ - `rails db:schema:dump` 产物不完整(索引/表选项返回空)——按「迁移先行、schema 只读」使用
90
+ - 运行时 DDL(`create_table` 等)能否穿透取决于代理的 SQL 翻译,生产建议 DDL 走达梦客户端
91
+ - 每语句经代理约有毫秒级额外开销,重 N+1 的代码路径建议用 `includes` 预加载
92
+
93
+ ## License
94
+
95
+ MIT
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DmRails
4
+ # ActiveRecord(mysql2 适配器)自省补丁。
5
+ #
6
+ # 背景:mysql2 适配器的表结构自省依赖 SHOW FULL FIELDS / SHOW KEYS /
7
+ # SHOW CREATE TABLE / information_schema,这些经代理透传到达梦(MySQL 兼容模式)
8
+ # 均不支持。列信息改由达梦侧辅助视图(sql/dm_rails_columns.sql)提供,
9
+ # 表清单用代理可自答的 SHOW TABLES。
10
+ module Introspection
11
+ def column_definitions(table_name)
12
+ sql = "SELECT fld, ctype, is_null, key_kind, def_val, extra_str FROM #{DmRails.view_name} " \
13
+ "WHERE tbl = #{quote(table_name.to_s.downcase)} ORDER BY col_order"
14
+ execute(sql, "SCHEMA").to_a.map do |row|
15
+ vals = row.is_a?(Hash) ? row.values : row
16
+ { Field: vals[0], Type: vals[1], Collation: "", Null: vals[2],
17
+ Key: vals[3].to_s, Default: dm_rails_normalize_default(vals[4]), Extra: vals[5].to_s,
18
+ Privileges: "", Comment: "" }
19
+ end
20
+ end
21
+
22
+ def primary_keys(table_name)
23
+ sql = "SELECT fld FROM #{DmRails.view_name} WHERE tbl = #{quote(table_name.to_s.downcase)} " \
24
+ "AND key_kind = 'PRI' ORDER BY col_order"
25
+ execute(sql, "SCHEMA").to_a.map { |row| (row.is_a?(Hash) ? row.values : row)[0] }
26
+ end
27
+
28
+ def data_sources
29
+ execute("SHOW TABLES", "SCHEMA").to_a.map { |row| (row.is_a?(Hash) ? row.values : row)[0] }
30
+ end
31
+
32
+ def tables
33
+ data_sources
34
+ end
35
+
36
+ def views
37
+ []
38
+ end
39
+
40
+ def data_source_exists?(name)
41
+ data_sources.include?(name.to_s.downcase)
42
+ end
43
+
44
+ def table_exists?(name)
45
+ data_source_exists?(name)
46
+ end
47
+
48
+ def view_exists?(_name)
49
+ false
50
+ end
51
+
52
+ # 索引/表选项仅用于 schema dump 与迁移生成,运行时不依赖;达梦链路无对应查询通道
53
+ def indexes(_table_name)
54
+ []
55
+ end
56
+
57
+ def table_options(_table_name)
58
+ {}
59
+ end
60
+
61
+ def table_comment(_table_name)
62
+ nil
63
+ end
64
+
65
+ # 达梦链路无法执行 SHOW CREATE TABLE(AR 解析生成列/默认值类型的兜底路径)
66
+ def create_table_info(_table_name)
67
+ ""
68
+ end
69
+
70
+ def default_type(_table_name, _field_name)
71
+ nil
72
+ end
73
+
74
+ private
75
+
76
+ # 达梦 DATA_DEFAULT 是原样表达式:字符串默认值带引号、可能带空白;
77
+ # 转成 MySQL SHOW FULL FIELDS 风格
78
+ def dm_rails_normalize_default(raw)
79
+ return nil if raw.nil?
80
+
81
+ v = raw.to_s.strip
82
+ return nil if v.empty? || v.casecmp("NULL").zero?
83
+ return "CURRENT_TIMESTAMP" if v =~ /\ACURRENT_TIMESTAMP(\(\))?\z/i || v =~ /\ANOW\(\)\z/i
84
+
85
+ v = Regexp.last_match(1) if v =~ /\A'(.*)'\z/m
86
+ v
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DmRails
4
+ class Railtie < ::Rails::Railtie
5
+ initializer "dm_rails.patch_mysql2_adapter" do
6
+ next unless DmRails.enabled?
7
+
8
+ ActiveSupport.on_load(:active_record) do
9
+ require "active_record/connection_adapters/mysql2_adapter"
10
+ require_relative "introspection"
11
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.prepend(DmRails::Introspection)
12
+ end
13
+ end
14
+
15
+ rake_tasks do
16
+ namespace :dm_rails do
17
+ desc "输出达梦侧辅助视图的建视图 SQL(请用达梦客户端在业务 schema 下执行)"
18
+ task :view_sql do
19
+ puts DmRails.view_sql
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DmRails
4
+ VERSION = "0.1.0"
5
+ end
data/lib/dm_rails.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dm_rails/version"
4
+
5
+ # dm-rails —— 让 Rails 经 MySQL 协议代理(ShardingSphere-Proxy)运行在达梦 DM8 上。
6
+ #
7
+ # 启用条件(满足其一):
8
+ # ENV["DM_RAILS_ENABLED"] = "1" (或兼容旧开关 ENV["DM_PROXY"] = "1")
9
+ #
10
+ # 可配置项(环境变量):
11
+ # DM_RAILS_VIEW —— 达梦侧列结构视图名,默认 dm_rails_columns(见 sql/dm_rails_columns.sql)
12
+ module DmRails
13
+ class << self
14
+ def enabled?
15
+ ENV["DM_RAILS_ENABLED"] == "1" || ENV["DM_PROXY"] == "1"
16
+ end
17
+
18
+ def view_name
19
+ ENV.fetch("DM_RAILS_VIEW", "dm_rails_columns")
20
+ end
21
+
22
+ # 达梦侧辅助视图的建视图 SQL(用达梦客户端在业务 schema 下执行,勿经代理执行)
23
+ def view_sql
24
+ File.read(File.expand_path("../sql/dm_rails_columns.sql", __dir__))
25
+ .gsub("dm_rails_columns", view_name)
26
+ end
27
+ end
28
+ end
29
+
30
+ require_relative "dm_rails/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,30 @@
1
+ -- dm-rails 辅助视图:为 ActiveRecord 提供 MySQL 风格的列结构信息
2
+ -- 在达梦业务 schema 下用达梦客户端执行(勿经代理执行);视图名可通过 DM_RAILS_VIEW 环境变量对应调整
3
+ CREATE OR REPLACE VIEW dm_rails_columns AS
4
+ SELECT LOWER(c.TABLE_NAME) AS tbl,
5
+ LOWER(c.COLUMN_NAME) AS fld,
6
+ CASE
7
+ WHEN c.DATA_TYPE = 'BIGINT' THEN 'bigint(20)'
8
+ WHEN c.DATA_TYPE IN ('INT','INTEGER') THEN 'int(11)'
9
+ WHEN c.DATA_TYPE = 'SMALLINT' THEN 'smallint(6)'
10
+ WHEN c.DATA_TYPE = 'TINYINT' THEN 'tinyint(4)'
11
+ WHEN c.DATA_TYPE = 'BIT' THEN 'tinyint(1)'
12
+ WHEN c.DATA_TYPE LIKE 'VARCHAR%' THEN CONCAT('varchar(', CAST(c.DATA_LENGTH AS VARCHAR(10)), ')')
13
+ WHEN c.DATA_TYPE LIKE 'CHAR%' THEN CONCAT('char(', CAST(c.DATA_LENGTH AS VARCHAR(10)), ')')
14
+ WHEN c.DATA_TYPE IN ('TEXT','CLOB','LONGVARCHAR') THEN 'text'
15
+ WHEN c.DATA_TYPE IN ('DATETIME','TIMESTAMP') THEN 'datetime'
16
+ WHEN c.DATA_TYPE = 'DATE' THEN 'date'
17
+ WHEN c.DATA_TYPE = 'TIME' THEN 'time'
18
+ WHEN c.DATA_TYPE IN ('DEC','DECIMAL','NUMERIC','NUMBER') THEN CONCAT('decimal(', CAST(NVL(c.DATA_PRECISION,10) AS VARCHAR(10)), ',', CAST(NVL(c.DATA_SCALE,0) AS VARCHAR(10)), ')')
19
+ WHEN c.DATA_TYPE = 'DOUBLE' THEN 'double'
20
+ WHEN c.DATA_TYPE = 'FLOAT' THEN 'float'
21
+ WHEN c.DATA_TYPE IN ('BLOB','IMAGE','LONGVARBINARY','VARBINARY','BINARY') THEN 'blob'
22
+ ELSE 'varchar(255)'
23
+ END AS ctype,
24
+ CASE c.NULLABLE WHEN 'Y' THEN 'YES' ELSE 'NO' END AS is_null,
25
+ NVL((SELECT 'PRI' FROM USER_CONS_COLUMNS cu JOIN USER_CONSTRAINTS uc ON cu.CONSTRAINT_NAME = uc.CONSTRAINT_NAME
26
+ WHERE uc.CONSTRAINT_TYPE = 'P' AND cu.TABLE_NAME = c.TABLE_NAME AND cu.COLUMN_NAME = c.COLUMN_NAME AND ROWNUM <= 1), '') AS key_kind,
27
+ c.DATA_DEFAULT AS def_val,
28
+ CASE WHEN LOWER(c.COLUMN_NAME) = 'id' THEN 'auto_increment' ELSE '' END AS extra_str,
29
+ c.COLUMN_ID AS col_order
30
+ FROM USER_TAB_COLUMNS c;
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dameng-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - qinyuanmao
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mysql2
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0.5'
41
+ description: DM8 has no Ruby driver or ActiveRecord adapter. dm-rails keeps the stock
42
+ mysql2 adapter and routes it through ShardingSphere-Proxy to DM8, patching ActiveRecord
43
+ schema introspection (SHOW FULL FIELDS / information_schema) with a DM-side helper
44
+ view. Battle-tested on a production Rails 7.1 system.
45
+ email:
46
+ - wandering.garfield@outlook.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - LICENSE
52
+ - README.md
53
+ - lib/dm_rails.rb
54
+ - lib/dm_rails/introspection.rb
55
+ - lib/dm_rails/railtie.rb
56
+ - lib/dm_rails/version.rb
57
+ - sql/dm_rails_columns.sql
58
+ homepage: https://github.com/qinyuanmao/dm-rails
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ homepage_uri: https://github.com/qinyuanmao/dm-rails
63
+ source_code_uri: https://github.com/qinyuanmao/dm-rails
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '2.7'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.4.19
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Run Ruby on Rails on DM (Dameng / 达梦) database via a MySQL-protocol proxy
83
+ test_files: []