barkest_core 1.5.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (308) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/Gemfile +22 -0
  4. data/Gemfile.lock +254 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +364 -0
  7. data/Rakefile +37 -0
  8. data/app/assets/fonts/barkest_core/ArchivoNarrow-Bold.ttf +0 -0
  9. data/app/assets/fonts/barkest_core/ArchivoNarrow-BoldItalic.ttf +0 -0
  10. data/app/assets/fonts/barkest_core/ArchivoNarrow-Italic.ttf +0 -0
  11. data/app/assets/fonts/barkest_core/ArchivoNarrow-Regular.ttf +0 -0
  12. data/app/assets/images/barkest_core/.keep +0 -0
  13. data/app/assets/images/barkest_core/barcode-B.svg +181 -0
  14. data/app/assets/javascripts/barkest_core/.keep +0 -0
  15. data/app/assets/javascripts/barkest_core/application.js +22 -0
  16. data/app/assets/javascripts/barkest_core/bootstrap-datepicker.js +1800 -0
  17. data/app/assets/javascripts/barkest_core/field_init.js +7 -0
  18. data/app/assets/javascripts/barkest_core/jquery.doubleScroll.js +112 -0
  19. data/app/assets/javascripts/barkest_core/masked_edit.js +25 -0
  20. data/app/assets/javascripts/barkest_core/system_status.js.erb +201 -0
  21. data/app/assets/stylesheets/barkest_core/.keep +0 -0
  22. data/app/assets/stylesheets/barkest_core/application.css +17 -0
  23. data/app/assets/stylesheets/barkest_core/custom.css.scss +264 -0
  24. data/app/assets/stylesheets/barkest_core/datepicker3.css +790 -0
  25. data/app/controllers/.keep +0 -0
  26. data/app/controllers/access_groups_controller.rb +74 -0
  27. data/app/controllers/account_activations_controller.rb +29 -0
  28. data/app/controllers/application_controller.rb +5 -0
  29. data/app/controllers/barkest_core/application_controller_base.rb +113 -0
  30. data/app/controllers/barkest_core/engine_controller_base.rb +15 -0
  31. data/app/controllers/barkest_core/testsub_controller.rb +21 -0
  32. data/app/controllers/contact_controller.rb +32 -0
  33. data/app/controllers/log_view_controller.rb +31 -0
  34. data/app/controllers/password_resets_controller.rb +126 -0
  35. data/app/controllers/sessions_controller.rb +64 -0
  36. data/app/controllers/status_controller.rb +150 -0
  37. data/app/controllers/system_config_controller.rb +238 -0
  38. data/app/controllers/system_update_controller.rb +164 -0
  39. data/app/controllers/test_access_controller.rb +44 -0
  40. data/app/controllers/test_report_controller.rb +75 -0
  41. data/app/controllers/users_controller.rb +218 -0
  42. data/app/helpers/.keep +0 -0
  43. data/app/helpers/barkest_core/application_helper.rb +134 -0
  44. data/app/helpers/barkest_core/form_helper.rb +469 -0
  45. data/app/helpers/barkest_core/html_helper.rb +70 -0
  46. data/app/helpers/barkest_core/misc_helper.rb +68 -0
  47. data/app/helpers/barkest_core/pdf_helper.rb +180 -0
  48. data/app/helpers/barkest_core/recaptcha_helper.rb +115 -0
  49. data/app/helpers/barkest_core/sessions_helper.rb +94 -0
  50. data/app/helpers/barkest_core/status_helper.rb +118 -0
  51. data/app/helpers/barkest_core/users_helper.rb +32 -0
  52. data/app/mailers/.keep +0 -0
  53. data/app/mailers/application_mailer.rb +5 -0
  54. data/app/mailers/barkest_core/application_mailer_base.rb +30 -0
  55. data/app/mailers/barkest_core/contact_form.rb +20 -0
  56. data/app/mailers/barkest_core/user_mailer.rb +44 -0
  57. data/app/models/.keep +0 -0
  58. data/app/models/access_group.rb +121 -0
  59. data/app/models/access_group_group_member.rb +13 -0
  60. data/app/models/access_group_user_member.rb +11 -0
  61. data/app/models/barkest_core/auth_config.rb +95 -0
  62. data/app/models/barkest_core/authorize_failure.rb +7 -0
  63. data/app/models/barkest_core/contact_message.rb +37 -0
  64. data/app/models/barkest_core/database_config.rb +223 -0
  65. data/app/models/barkest_core/db_table.rb +21 -0
  66. data/app/models/barkest_core/email_config.rb +132 -0
  67. data/app/models/barkest_core/global_status.rb +267 -0
  68. data/app/models/barkest_core/log_entry.rb +101 -0
  69. data/app/models/barkest_core/log_view_options.rb +51 -0
  70. data/app/models/barkest_core/ms_sql_db_definition.rb +441 -0
  71. data/app/models/barkest_core/ms_sql_definition.rb +221 -0
  72. data/app/models/barkest_core/ms_sql_function.rb +423 -0
  73. data/app/models/barkest_core/not_logged_in.rb +7 -0
  74. data/app/models/barkest_core/pdf_table_builder.rb +407 -0
  75. data/app/models/barkest_core/self_update_config.rb +37 -0
  76. data/app/models/barkest_core/user_alert.rb +29 -0
  77. data/app/models/barkest_core/user_alert_generators.rb +58 -0
  78. data/app/models/barkest_core/user_manager.rb +404 -0
  79. data/app/models/barkest_core/work_path.rb +74 -0
  80. data/app/models/disable_user.rb +18 -0
  81. data/app/models/ldap_access_group.rb +15 -0
  82. data/app/models/system_config.rb +99 -0
  83. data/app/models/user.rb +405 -0
  84. data/app/models/user_login_history.rb +11 -0
  85. data/app/views/.keep +0 -0
  86. data/app/views/access_groups/_form.html.erb +19 -0
  87. data/app/views/access_groups/edit.html.erb +2 -0
  88. data/app/views/access_groups/index.html.erb +32 -0
  89. data/app/views/access_groups/new.html.erb +2 -0
  90. data/app/views/access_groups/show.html.erb +4 -0
  91. data/app/views/barkest_core/contact_form/contact.html.erb +16 -0
  92. data/app/views/barkest_core/contact_form/contact.text.erb +13 -0
  93. data/app/views/barkest_core/testsub/_links.html.erb +5 -0
  94. data/app/views/barkest_core/testsub/page1.html.erb +3 -0
  95. data/app/views/barkest_core/testsub/page2.html.erb +2 -0
  96. data/app/views/barkest_core/testsub/page3.html.erb +2 -0
  97. data/app/views/barkest_core/user_mailer/account_activation.html.erb +7 -0
  98. data/app/views/barkest_core/user_mailer/account_activation.text.erb +6 -0
  99. data/app/views/barkest_core/user_mailer/invalid_password_reset.html.erb +3 -0
  100. data/app/views/barkest_core/user_mailer/invalid_password_reset.text.erb +5 -0
  101. data/app/views/barkest_core/user_mailer/password_reset.html.erb +8 -0
  102. data/app/views/barkest_core/user_mailer/password_reset.text.erb +7 -0
  103. data/app/views/contact/index.html.erb +24 -0
  104. data/app/views/layouts/_footer_copyright.html.erb +1 -0
  105. data/app/views/layouts/_menu_admin.html.erb +5 -0
  106. data/app/views/layouts/_menu_anon.html.erb +0 -0
  107. data/app/views/layouts/_menu_auth.html.erb +3 -0
  108. data/app/views/layouts/_menu_footer.html.erb +1 -0
  109. data/app/views/layouts/_nav_logo.html.erb +1 -0
  110. data/app/views/layouts/application.html.erb +2 -0
  111. data/app/views/layouts/barkest_core/_application.html.erb +24 -0
  112. data/app/views/layouts/barkest_core/_footer.html.erb +18 -0
  113. data/app/views/layouts/barkest_core/_header.html.erb +38 -0
  114. data/app/views/layouts/barkest_core/_html_mailer.html.erb +11 -0
  115. data/app/views/layouts/barkest_core/_menu_account.html.erb +14 -0
  116. data/app/views/layouts/barkest_core/_menu_sample.html.erb +1 -0
  117. data/app/views/layouts/barkest_core/_messages.html.erb +4 -0
  118. data/app/views/layouts/barkest_core/_shim.html.erb +4 -0
  119. data/app/views/layouts/barkest_core/_subheader.html.erb +1 -0
  120. data/app/views/layouts/barkest_core/_text_mailer.text.erb +4 -0
  121. data/app/views/layouts/mailer.html.erb +1 -0
  122. data/app/views/layouts/mailer.text.erb +1 -0
  123. data/app/views/log_view/index.html.erb +100 -0
  124. data/app/views/password_resets/edit.html.erb +20 -0
  125. data/app/views/password_resets/new.html.erb +14 -0
  126. data/app/views/sessions/new.html.erb +27 -0
  127. data/app/views/shared/_error_messages.html.erb +29 -0
  128. data/app/views/shared/_generic_user_alert.html.erb +4 -0
  129. data/app/views/status/current.html.erb +34 -0
  130. data/app/views/status/test.html.erb +50 -0
  131. data/app/views/system_config/index.html.erb +25 -0
  132. data/app/views/system_config/show_auth.html.erb +28 -0
  133. data/app/views/system_config/show_database.html.erb +36 -0
  134. data/app/views/system_config/show_email.html.erb +21 -0
  135. data/app/views/system_config/show_self_update.html.erb +13 -0
  136. data/app/views/system_update/index.html.erb +31 -0
  137. data/app/views/system_update/new.html.erb +2 -0
  138. data/app/views/test_access/allow_anon.html.erb +2 -0
  139. data/app/views/test_access/require_admin.html.erb +2 -0
  140. data/app/views/test_access/require_group_x.html.erb +2 -0
  141. data/app/views/test_access/require_user.html.erb +2 -0
  142. data/app/views/test_report/index.csv.csvrb +23 -0
  143. data/app/views/test_report/index.html.erb +6 -0
  144. data/app/views/test_report/index.pdf.prawn +50 -0
  145. data/app/views/test_report/index.xlsx.axlsx +28 -0
  146. data/app/views/users/_user.html.erb +57 -0
  147. data/app/views/users/_user_details.html.erb +15 -0
  148. data/app/views/users/_user_details_for_list.html.erb +1 -0
  149. data/app/views/users/_user_form.html.erb +13 -0
  150. data/app/views/users/disable_confirm.html.erb +19 -0
  151. data/app/views/users/edit.html.erb +15 -0
  152. data/app/views/users/index.html.erb +9 -0
  153. data/app/views/users/new.html.erb +10 -0
  154. data/app/views/users/show.html.erb +46 -0
  155. data/bin/rails +12 -0
  156. data/config/routes.rb +3 -0
  157. data/db/migrate/20160617172539_create_access_groups.rb +10 -0
  158. data/db/migrate/20160617172725_create_users.rb +26 -0
  159. data/db/migrate/20160617172833_create_user_login_histories.rb +12 -0
  160. data/db/migrate/20160622151720_create_access_group_user_members.rb +9 -0
  161. data/db/migrate/20160622151925_create_access_group_group_members.rb +9 -0
  162. data/db/migrate/20160701005706_create_ldap_access_groups.rb +11 -0
  163. data/db/migrate/20161108155029_create_system_configs.rb +11 -0
  164. data/db/seeds/barkest_core_01_create_users.rb +42 -0
  165. data/db/seeds.rb +53 -0
  166. data/lib/barkest_core/concerns/association_with_defaults.rb +55 -0
  167. data/lib/barkest_core/concerns/boolean_parser.rb +88 -0
  168. data/lib/barkest_core/concerns/date_parser.rb +181 -0
  169. data/lib/barkest_core/concerns/email_tester.rb +55 -0
  170. data/lib/barkest_core/concerns/encrypted_fields.rb +156 -0
  171. data/lib/barkest_core/concerns/named_model.rb +73 -0
  172. data/lib/barkest_core/concerns/number_parser.rb +145 -0
  173. data/lib/barkest_core/concerns/utc_conversion.rb +60 -0
  174. data/lib/barkest_core/engine.rb +105 -0
  175. data/lib/barkest_core/extensions/active_record_extensions.rb +120 -0
  176. data/lib/barkest_core/extensions/application_configuration_extensions.rb +38 -0
  177. data/lib/barkest_core/extensions/application_extensions.rb +50 -0
  178. data/lib/barkest_core/extensions/axlsx_extenstions.rb +157 -0
  179. data/lib/barkest_core/extensions/fixture_set_extensions.rb +107 -0
  180. data/lib/barkest_core/extensions/generator_extensions.rb +271 -0
  181. data/lib/barkest_core/extensions/main_app_extensions.rb +35 -0
  182. data/lib/barkest_core/extensions/prawn_document_extensions.rb +367 -0
  183. data/lib/barkest_core/extensions/prawn_table_extensions.rb +131 -0
  184. data/lib/barkest_core/extensions/router_extensions.rb +106 -0
  185. data/lib/barkest_core/extensions/simple_formatter_extensions.rb +66 -0
  186. data/lib/barkest_core/extensions/test_case_extensions.rb +348 -0
  187. data/lib/barkest_core/extensions/time_extensions.rb +164 -0
  188. data/lib/barkest_core/handlers/csv_handler.rb +30 -0
  189. data/lib/barkest_core/version.rb +3 -0
  190. data/lib/barkest_core.rb +324 -0
  191. data/lib/generators/barkest/install_generator.rb +102 -0
  192. data/lib/generators/barkest_core/actions/01_patch_application_controller.rb +55 -0
  193. data/lib/generators/barkest_core/actions/02_patch_application_mailer.rb +56 -0
  194. data/lib/generators/barkest_core/actions/03_patch_assets.rb +62 -0
  195. data/lib/generators/barkest_core/actions/04_patch_layouts.rb +36 -0
  196. data/lib/generators/barkest_core/actions/05_patch_routes.rb +93 -0
  197. data/lib/generators/barkest_core/actions/06_patch_seeds.rb +60 -0
  198. data/lib/generators/barkest_core/actions/07_copy_migrations.rb +51 -0
  199. data/lib/generators/barkest_core/actions/08_configure_database.rb +52 -0
  200. data/lib/generators/barkest_core/actions/09_configure_secrets.rb +29 -0
  201. data/lib/generators/barkest_core/actions/99_patch_gitignore.rb +57 -0
  202. data/lib/generators/barkest_core/install_generator.rb +17 -0
  203. data/test/barkest_core_test.rb +83 -0
  204. data/test/controllers/access_groups_controller_test.rb +53 -0
  205. data/test/controllers/contact_controller_test.rb +10 -0
  206. data/test/controllers/sessions_controller_test.rb +10 -0
  207. data/test/controllers/users_controller_test.rb +10 -0
  208. data/test/dummy/.gitignore +10 -0
  209. data/test/dummy/README.rdoc +28 -0
  210. data/test/dummy/Rakefile +6 -0
  211. data/test/dummy/app/assets/images/.keep +0 -0
  212. data/test/dummy/app/assets/javascripts/application.js +14 -0
  213. data/test/dummy/app/assets/stylesheets/application.css +16 -0
  214. data/test/dummy/app/controllers/application_controller.rb +5 -0
  215. data/test/dummy/app/controllers/concerns/.keep +0 -0
  216. data/test/dummy/app/helpers/application_helper.rb +2 -0
  217. data/test/dummy/app/mailers/.keep +0 -0
  218. data/test/dummy/app/mailers/application_mailer.rb +3 -0
  219. data/test/dummy/app/models/.keep +0 -0
  220. data/test/dummy/app/models/concerns/.keep +0 -0
  221. data/test/dummy/app/views/layouts/application.html.erb +1 -0
  222. data/test/dummy/app/views/layouts/mailer.html.erb +1 -0
  223. data/test/dummy/app/views/layouts/mailer.text.erb +1 -0
  224. data/test/dummy/app/views/system_config/show_fake.html.erb +3 -0
  225. data/test/dummy/bin/bundle +3 -0
  226. data/test/dummy/bin/rails +4 -0
  227. data/test/dummy/bin/rake +4 -0
  228. data/test/dummy/bin/setup +29 -0
  229. data/test/dummy/config/application.rb +27 -0
  230. data/test/dummy/config/boot.rb +5 -0
  231. data/test/dummy/config/environment.rb +5 -0
  232. data/test/dummy/config/environments/development.rb +47 -0
  233. data/test/dummy/config/environments/production.rb +79 -0
  234. data/test/dummy/config/environments/test.rb +44 -0
  235. data/test/dummy/config/initializers/assets.rb +11 -0
  236. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  237. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  238. data/test/dummy/config/initializers/db_updater_ext.rb +33 -0
  239. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  240. data/test/dummy/config/initializers/inflections.rb +16 -0
  241. data/test/dummy/config/initializers/mime_types.rb +4 -0
  242. data/test/dummy/config/initializers/session_store.rb +3 -0
  243. data/test/dummy/config/initializers/sys_config_ext.rb +12 -0
  244. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  245. data/test/dummy/config/locales/en.yml +23 -0
  246. data/test/dummy/config/routes.rb +60 -0
  247. data/test/dummy/config.ru +4 -0
  248. data/test/dummy/db/schema.rb +95 -0
  249. data/test/dummy/db/seeds/barkest_core_01_create_users.rb +42 -0
  250. data/test/dummy/db/seeds.rb +51 -0
  251. data/test/dummy/lib/assets/.keep +0 -0
  252. data/test/dummy/log/.keep +0 -0
  253. data/test/dummy/public/404.html +67 -0
  254. data/test/dummy/public/422.html +67 -0
  255. data/test/dummy/public/500.html +66 -0
  256. data/test/dummy/public/favicon.ico +0 -0
  257. data/test/dummy/sql/my_test_view.sql +3 -0
  258. data/test/fixtures/access_groups.yml +21 -0
  259. data/test/fixtures/users.yml +71 -0
  260. data/test/helpers/barkest_core/sessions_helper_test.rb +22 -0
  261. data/test/integration/access_group_mgmt_test.rb +33 -0
  262. data/test/integration/access_test.rb +24 -0
  263. data/test/integration/account_activations_access_test.rb +12 -0
  264. data/test/integration/contact_test.rb +98 -0
  265. data/test/integration/extra_partial_test.rb +41 -0
  266. data/test/integration/log_view_access_test.rb +12 -0
  267. data/test/integration/password_resets_test.rb +101 -0
  268. data/test/integration/reports_test.rb +53 -0
  269. data/test/integration/status_access_test.rb +27 -0
  270. data/test/integration/system_config_access_test.rb +24 -0
  271. data/test/integration/system_update_access_test.rb +19 -0
  272. data/test/integration/users_access_test.rb +34 -0
  273. data/test/integration/users_edit_test.rb +178 -0
  274. data/test/integration/users_index_test.rb +62 -0
  275. data/test/integration/users_login_test.rb +67 -0
  276. data/test/integration/users_signup_test.rb +54 -0
  277. data/test/mailers/.keep +0 -0
  278. data/test/mailers/barkest_core/contact_form_test.rb +28 -0
  279. data/test/mailers/barkest_core/user_mailer_test.rb +43 -0
  280. data/test/mailers/previews/barkest_core/contact_form_preview.rb +17 -0
  281. data/test/mailers/previews/barkest_core/user_mailer_preview.rb +26 -0
  282. data/test/models/access_group_group_member_test.rb +28 -0
  283. data/test/models/access_group_test.rb +114 -0
  284. data/test/models/access_group_user_member_test.rb +28 -0
  285. data/test/models/barkest_core/auth_config_test.rb +57 -0
  286. data/test/models/barkest_core/bool_parser_test.rb +28 -0
  287. data/test/models/barkest_core/contact_message_test.rb +61 -0
  288. data/test/models/barkest_core/database_config_test.rb +33 -0
  289. data/test/models/barkest_core/date_parser_test.rb +110 -0
  290. data/test/models/barkest_core/email_config_test.rb +57 -0
  291. data/test/models/barkest_core/global_status_test.rb +50 -0
  292. data/test/models/barkest_core/ms_sql_db_updater_test.rb +115 -0
  293. data/test/models/barkest_core/ms_sql_definition_test.rb +102 -0
  294. data/test/models/barkest_core/ms_sql_function_test.rb +131 -0
  295. data/test/models/barkest_core/number_parser_test.rb +29 -0
  296. data/test/models/barkest_core/self_update_config_test.rb +29 -0
  297. data/test/models/barkest_core/user_alert_test.rb +19 -0
  298. data/test/models/barkest_core/user_manager_test.rb +34 -0
  299. data/test/models/barkest_core/work_path_test.rb +26 -0
  300. data/test/models/disable_user_test.rb +27 -0
  301. data/test/models/generic_time_test.rb +66 -0
  302. data/test/models/ldap_access_group_test.rb +31 -0
  303. data/test/models/pdf_table_builder_test.rb +6 -0
  304. data/test/models/system_config_test.rb +78 -0
  305. data/test/models/user_login_history_test.rb +37 -0
  306. data/test/models/user_test.rb +130 -0
  307. data/test/test_helper.rb +63 -0
  308. metadata +798 -0
@@ -0,0 +1,221 @@
1
+ require 'zlib'
2
+
3
+ module BarkestCore
4
+
5
+ ##
6
+ # Contains a SQL definition to create a single table, view, function, or procedure.
7
+ #
8
+ # SQL source is not validated, however simple checks are made to ensure that only
9
+ # one DDL statement is present unless you are creating a procedure in which case this
10
+ # check is skipped.
11
+ #
12
+ # To reference another object in your definition, prefix @Z~ to the beginning of the
13
+ # object name. For instance 'SELECT * FROM @Z~my_table' could be expanded to
14
+ # 'SELECT * FROM zz_barkest_core_my_table'.
15
+ #
16
+ # Function return types are grabbed as well so you know if your function is returning
17
+ # a table or an integral type.
18
+ class MsSqlDefinition
19
+ InvalidDefinition = Class.new(StandardError)
20
+
21
+ EmptyDefinition = Class.new(InvalidDefinition)
22
+ MissingCreateStatement = Class.new(InvalidDefinition)
23
+ ExtraDDL = Class.new(InvalidDefinition)
24
+ UnmatchedBracket = Class.new(InvalidDefinition)
25
+ UnclosedQuote = Class.new(InvalidDefinition)
26
+ UnmatchedComment = Class.new(InvalidDefinition)
27
+ MissingReturnType = Class.new(InvalidDefinition)
28
+
29
+ attr_accessor :name_prefix
30
+
31
+ attr_reader :command, :name, :type, :definition, :version, :return_type, :source_location
32
+
33
+ def initialize(raw_sql, source = '', default_timestamp = 0)
34
+
35
+ @source_location = source.to_s
36
+ @return_type = :table # the default. functions can be different. procedures can be iffy since they may or may not return a result set.
37
+ @command = 'CREATE'
38
+
39
+ if default_timestamp.is_a?(String)
40
+ default_timestamp = Time.utc_parse(default_timestamp)
41
+ end
42
+
43
+ if default_timestamp.is_a?(Date)
44
+ default_timestamp = (default_timestamp.strftime('%Y%m%d') + '0000').to_i
45
+ elsif default_timestamp.is_a?(Time)
46
+ default_timestamp = "#{default_timestamp.year.to_s.rjust(4,'0')}#{default_timestamp.month.to_s.rjust(2,'0')}#{default_timestamp.day.to_s.rjust(2,'0')}#{default_timestamp.hour.to_s.rjust(2,'0')}#{default_timestamp.min.to_s.rjust(2,'0')}".to_i
47
+ end
48
+
49
+ default_timestamp = 0 unless default_timestamp.is_a?(Fixnum)
50
+
51
+ ts_regex = /^--\s*(?<YR>\d{4})-?(?<MON>\d{2})-?(?<DAY>\d{2})\s*(?:(?<HR>\d{2}):?(?<MIN>\d{2})?)?\s*$/
52
+
53
+ timestamp = nil
54
+
55
+ raw_sql = raw_sql.to_s.lstrip
56
+ # strip leading comment lines.
57
+ while raw_sql[0...2] == '--' || raw_sql[0...2] == '/*'
58
+ if raw_sql[0...2] == '--'
59
+ # trim off the first line.
60
+ first_line,_,raw_sql = raw_sql.partition("\n").map {|v| v.strip}
61
+ raw_sql ||= ''
62
+ unless timestamp
63
+ if (ts = ts_regex.match(first_line))
64
+ timestamp = "#{ts['YR']}#{ts['MON']}#{ts['DAY']}#{ts['HR'] || '00'}#{ts['MIN'] || '00'}".to_i
65
+ end
66
+ end
67
+ else
68
+ # find the first */ sequence in the string.
69
+ comment_end = raw_sql.index('*/')
70
+ raise UnmatchedComment, 'raw_sql starts with "/*" sequence with no matching "*/" sequence' unless comment_end
71
+
72
+ # find the last /* sequence before that.
73
+ comment_start = raw_sql.rindex('/*', comment_end)
74
+
75
+ # remove this comment
76
+ raw_sql = (raw_sql[0...comment_start].to_s + raw_sql[(comment_end + 2)..-1].to_s).lstrip
77
+ end
78
+ end
79
+
80
+ timestamp ||= default_timestamp
81
+
82
+ raise EmptyDefinition, 'raw_sql contains no data' if raw_sql.blank?
83
+
84
+ # first line should be CREATE VIEW|FUNCTION|PROCEDURE "XYZ"
85
+ # or ALTER TABLE "XYZ"
86
+ regex = /^(?:(?<CMD>ALTER)\s+(?<TYPE>TABLE)|(?<CMD>CREATE)\s+(?<TYPE>TABLE|VIEW|FUNCTION|PROCEDURE))\s+["\[]?(?<NAME>[A-Z][A-Z0-9_]*)["\]]?\s+(?<DEFINITION>.*)$/im
87
+ match = regex.match(raw_sql)
88
+
89
+ raise MissingCreateStatement, 'raw_sql must contain a "CREATE|ALTER VIEW|FUNCTION|PROCEDURE" statement' unless match
90
+
91
+ @command = match['CMD'].upcase
92
+ @type = match['TYPE'].upcase
93
+ @name = match['NAME']
94
+ @definition = match['DEFINITION'].strip
95
+
96
+ # we're going to test the definition loosely.
97
+ # so first we'll get rid of all valid literals and comments.
98
+ # this will leave behind mangled invalid SQL, but we can use it to determine if there are any simple issues.
99
+ # all removed components are replaced by single spaces to ensure that the remaining components are separate from
100
+ # each other.
101
+ test_sql = match['DEFINITION']
102
+ .gsub(/\s+/,' ') # condense whitespace
103
+ .split(/(?:(?:'[^']*')|(?:"[^"]*")|(?:\[[^\[\]]*\]))/m).join(' ') # remove all quoted literals '', "", and []
104
+ .split(/--[^\r\n]*/).join(' ') # remove all single-line comments
105
+
106
+ # remove all multi-line comments
107
+ # the regex will find matches for all of the innermost multi-line comments.
108
+ regex = /\/\*(?:(?:[^\/\*])|(?:\/[^\*]))*\*\//m
109
+
110
+ # so we go through removing them until there are no longer any matches.
111
+ while regex.match(test_sql)
112
+ test_sql = test_sql.split(regex).join(' ')
113
+ end
114
+
115
+ # now we can test for a number of missing items nice and easily.
116
+ raise UnmatchedBracket, 'raw_sql contains an opening bracket with no closing bracket' if test_sql.include?('[')
117
+ raise UnmatchedBracket, 'raw_sql contains a closing bracket with no opening bracket' if test_sql.include?(']')
118
+ raise UnclosedQuote, 'raw_sql contains an unclosed string literal' if test_sql.include?("'")
119
+ raise UnclosedQuote, 'raw_sql contains an unclosed ANSI quoted literal' if test_sql.include?('"')
120
+ raise UnmatchedComment, 'raw_sql contains a "/*" sequence with no matching "*/" sequence' if test_sql.include?('/*')
121
+ raise UnmatchedComment, 'raw_sql contains a "*/" sequence with no matching "/*" sequence' if test_sql.include?('*/')
122
+
123
+ unless type == 'PROCEDURE'
124
+ # and finally we can test for extra DDL.
125
+ # only the initial CREATE statement is allowed.
126
+ regex = /\s(create|alter|drop|grant)\s/im
127
+ if (match = regex.match(test_sql))
128
+ raise ExtraDDL, "raw_sql contains a #{match[1]} statement after the initial CREATE statement"
129
+ end
130
+ end
131
+
132
+ # and for functions, get the return type.
133
+ if type == 'FUNCTION'
134
+ regex = /\sRETURNS\s+(?:@[A-Z][A-Z0-9_]*\s+)?(?<TYPE>[A-Z][A-Z0-9_()]*)\s/im
135
+
136
+ match = regex.match(@definition)
137
+ raise MissingReturnType, 'raw_sql seems to be missing the RETURNS statement for the function.' unless match
138
+
139
+ rtype = match['TYPE'].downcase
140
+ rsize = 0
141
+ if rtype.include?('(')
142
+ rtype,_,rsize = rtype.partition('(')
143
+ rsize = rsize.partition(')')[0].to_i
144
+ end
145
+
146
+ @return_type =
147
+ case rtype
148
+ when 'bit'
149
+ :boolean
150
+
151
+ when 'int', 'integer', 'bigint', 'smallint', 'tinyint'
152
+ :integer
153
+
154
+ when 'decimal', 'numeric', 'money', 'smallmoney'
155
+ :decimal
156
+
157
+ when 'float', 'real'
158
+ :float
159
+
160
+ when 'date', 'datetime', 'datetime2', 'time', 'smalldatetime', 'datetimeoffset'
161
+ :time
162
+
163
+ when 'char', 'varchar', 'text', 'nchar', 'nvarchar', 'ntext', 'binary', 'varbinary', 'image'
164
+ :string
165
+
166
+ else
167
+ rtype.to_sym
168
+ end
169
+ end
170
+
171
+ # set the version.
172
+ @version = timestamp.to_s.ljust(12, '0') + Zlib.crc32(@definition).to_s(16).ljust(8,'0').upcase
173
+ end
174
+
175
+ def prefixed_name
176
+ prefix = name_prefix.to_s
177
+ return name if prefix == ''
178
+ return name if name.index(prefix) == 0
179
+ prefix + name
180
+ end
181
+
182
+ def is_create?
183
+ command == 'CREATE'
184
+ end
185
+
186
+ def update_sql
187
+ "#{command} #{type} \"#{prefixed_name}\"\n#{definition.gsub('@Z~',name_prefix)}"
188
+ end
189
+
190
+ def drop_sql
191
+ "DROP #{type} \"#{prefixed_name}\""
192
+ end
193
+
194
+ def grant_sql(user_name)
195
+ sel_exec = if type == 'PROCEDURE'
196
+ 'EXECUTE'
197
+ elsif type == 'FUNCTION' && return_type != :table
198
+ 'EXECUTE'
199
+ elsif type == 'TABLE'
200
+ 'SELECT, INSERT, UPDATE, DELETE'
201
+ else
202
+ 'SELECT'
203
+ end
204
+
205
+ "GRANT VIEW DEFINITION,#{sel_exec} ON \"#{prefixed_name}\" TO \"#{user_name}\""
206
+ end
207
+
208
+ def to_s
209
+ "#{type} #{name}"
210
+ end
211
+
212
+ def ==(other)
213
+ return false unless other.is_a?(MsSqlDefinition)
214
+ return false unless other.name == name
215
+ return false unless other.type == type
216
+ return false unless other.definition == definition
217
+ true
218
+ end
219
+
220
+ end
221
+ end
@@ -0,0 +1,423 @@
1
+ module BarkestCore
2
+
3
+ ##
4
+ # This class provides a model-like interface to SQL Server User Defined Functions.
5
+ #
6
+ # It's understandable that in terms of separation of concerns, logic has no place in the database.
7
+ # In the SQL Server world, UDFs cannot make changes to data, they can only present it.
8
+ # With that in mind, I consider UDFs to be parameterized queries, that are often times orders of magnitude
9
+ # faster than trying to construct a query via ActiveRecord.
10
+ #
11
+ # Although "models" inheriting from this class are not ActiveRecord models, this class does include
12
+ # ActiveModel::Model and ActiveModel::Validations to allow you to construct your UDF models with similar attributes
13
+ # to ActiveRecord models. For instance, you can ensure that returned values meet certain requirements using the
14
+ # validations. This allows you to further remove logic from the database and still gain the benefit of running
15
+ # a parameterized query.
16
+ class MsSqlFunction
17
+
18
+ InvalidConnection = Class.new(StandardError)
19
+
20
+ include ActiveModel::Model
21
+ include ActiveModel::Validations
22
+
23
+ include DateParser
24
+ include NumberParser
25
+ include BooleanParser
26
+
27
+ ##
28
+ # Sets the connection handler to use for this function.
29
+ #
30
+ # The default behavior is to piggyback on the ActiveRecord::Base connections.
31
+ # To override this behavior, provide a class that responds to the :connection method.
32
+ #
33
+ # class MyFunction < MsSqlFunction
34
+ # use_connection SomeMsSqlTable
35
+ # end
36
+ #
37
+ def self.use_connection(connected_object)
38
+ @conn_handler =
39
+ if connected_object.is_a?(Class)
40
+ connected_object
41
+ else
42
+ const_get(connected_object || 'ActiveRecord::Base')
43
+ end
44
+ raise ArgumentError.new('Connected object must be a class or class name.') unless @conn_handler
45
+ raise ArgumentError.new('Connected object must respond to :connection') unless @conn_handler.respond_to?(:connection)
46
+ end
47
+
48
+ ##
49
+ # Gets a connection from the connection handler.
50
+ #
51
+ # The connection must be to a SQL Server since this class has no idea how to work with UDFs in any other language at this time.
52
+ #
53
+ def self.connection
54
+ conn = connection_handler.connection
55
+ raise InvalidConnection unless conn.is_a?(ActiveRecord::ConnectionAdapters::SQLServerAdapter)
56
+ conn
57
+ end
58
+
59
+ ##
60
+ # Gets the UDF name for this class.
61
+ #
62
+ # It is important that you do not set this on the MsSqlFunction class itself.
63
+ #
64
+ def self.function_name
65
+ return '(none)' if self == MsSqlFunction
66
+ @udf ||= ''
67
+ end
68
+
69
+ ##
70
+ # Sets the UDF name for this class.
71
+ #
72
+ # It is important that you do not set this on the MsSqlFunction class itself.
73
+ #
74
+ def self.function_name=(value)
75
+ raise StandardError.new("Function name for #{self} cannot be set.") if self == MsSqlFunction
76
+ raise StandardError.new("Function name for #{self} cannot be set more than once.") unless function_name.blank?
77
+ @udf = process_udf(value)
78
+ end
79
+
80
+ ##
81
+ # Returns parameter information for the UDF.
82
+ #
83
+ # The returned hash contains the most important attributes for most applications including :type, :data_type, and :default.
84
+ #
85
+ def self.parameters
86
+ @param_info.inject({}) { |memo,(k,v)| memo[k] = { type: v[:type], data_type: v[:data_type], default: v[:default] }; memo }
87
+ end
88
+
89
+ ##
90
+ # Sets the default values for parameters.
91
+ #
92
+ # The +values+ should be a hash using the parameter name as the key and the default as the value.
93
+ # The easiest way to ensure it works is to set the defaults in the hash returned from #parameters.
94
+ #
95
+ def self.parameters=(values)
96
+ if values && values.is_a?(Hash)
97
+ values.each do |k,v|
98
+ if @param_info[k]
99
+ if v.is_a?(Hash)
100
+ @param_info[k][:default] = v[:default]
101
+ else
102
+ @param_info[k][:default] = v
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ ##
110
+ # Gets the column information for the UDF.
111
+ def self.columns
112
+ @column_info
113
+ end
114
+
115
+ ##
116
+ # Selects the data from the UDF using the provided parameters.
117
+ #
118
+ # MyFunction.select(user: 'john', day_of_week: 3)
119
+ #
120
+ # Returns an array containing the rows returned.
121
+ #
122
+ def self.select(params = {})
123
+
124
+ args = []
125
+
126
+ params = {} unless params.is_a?(Hash)
127
+
128
+ @param_info.each do |k,v|
129
+ args[v[:ordinal]] = [ v[:data_type], self.send(v[:format], params[k] || v[:default]) ]
130
+ end
131
+
132
+ where = ''
133
+ idx = args.count
134
+ params.each do |k,v|
135
+ unless @param_info.include? k
136
+ where += ' AND ' unless where.blank?
137
+ where += "([#{k}]"
138
+ if v.is_a? Array
139
+ # IN clause
140
+ where += ' IN (' + v.map{ |value| quote_param(value)[0] }.join(', ') + ')'
141
+ elsif v.is_a? Hash
142
+ if v.include? :between
143
+ v = v[:between]
144
+ raise ArgumentError.new("between clause for #{k} requires an array argument") unless v.is_a? Array
145
+ where += " BETWEEN @#{idx} AND @#{idx + 1}"
146
+ value,type = quote_param(v[0])
147
+ args[idx] = [ type, value ]
148
+ value,type = quote_param(v[1])
149
+ args[idx + 1] = [ type, value ]
150
+ idx += 2
151
+ elsif v.include? :like
152
+ where += " LIKE @#{idx}"
153
+ value,type = quote_param(v[:like].to_s)
154
+ args[idx] = [ type, value ]
155
+ idx += 1
156
+ else
157
+ operator = nil
158
+ value = nil
159
+ { not: '<>', lt: '<', lte: '<=', gt: '>', gte: '>=', eq: '=' }.each do |key,op|
160
+ if v.include? key
161
+ operator = op
162
+ value = v[key]
163
+ break
164
+ end
165
+ end
166
+ raise ArgumentError.new("unknown clause for #{k}") unless operator
167
+ where += " #{operator} @#{idx}"
168
+ value,type = quote_param(value)
169
+ args[idx] = [ type, value ]
170
+ idx += 1
171
+ end
172
+ else
173
+ where += " = @#{idx}"
174
+ value,type = quote_param(v)
175
+ args[idx] = [ type, value ]
176
+ idx += 1
177
+ end
178
+ where += ')'
179
+ end
180
+ end
181
+
182
+ sql = "SELECT * FROM #{@udf}(#{@udf_args})"
183
+ sql += " WHERE #{where}" unless where.blank?
184
+
185
+ ret = []
186
+
187
+ execute(sql, args) do |row|
188
+ ret << self.new(row)
189
+ end
190
+
191
+ ret
192
+ end
193
+
194
+
195
+ private
196
+
197
+ def self.quote_param(value)
198
+ if value.nil?
199
+ [ 'NULL', 'varchar(1)' ]
200
+ elsif value.is_a? Integer
201
+ [ value.to_s, 'integer' ]
202
+ elsif value.is_a? Float
203
+ [ value.to_s, 'float' ]
204
+ elsif value.is_a?(Date) || value.is_a?(Time)
205
+ [ value.strftime('%Y-%m-%d %H:%M:%S'), 'datetime' ]
206
+ elsif value.is_a? TrueClass
207
+ [ 1, 'bit' ]
208
+ elsif value.is_a? FalseClass
209
+ [ 0, 'bit' ]
210
+ else
211
+ [ "'#{value.to_s.gsub('\'','\'\'')}'", 'varchar(max)' ]
212
+ end
213
+ end
214
+
215
+ def self.instrumenter
216
+ @instrumenter ||= ActiveSupport::Notifications.instrumenter
217
+ end
218
+
219
+
220
+ def self.execute(sql, binds)
221
+ sql = "EXEC sp_executesql N'#{sql.gsub('\'','\'\'')}'"
222
+
223
+ unless binds.blank?
224
+ binds.each_with_index do |v,i|
225
+ sql += i == 0 ? ', N\'' : ', '
226
+ sql += "@#{i} #{v[0]}"
227
+ end
228
+ sql += '\''
229
+ binds.each_with_index do |v,i|
230
+ sql += ", @#{i}=#{v[1]}"
231
+ end
232
+ end
233
+
234
+ ret = []
235
+
236
+ conn = connection
237
+ instrumenter.instrument(
238
+ "sql.active_record",
239
+ :sql => sql,
240
+ :name => 'SQL',
241
+ :connection_id => conn.object_id,
242
+ :statement_name => nil,
243
+ :binds => nil) do
244
+ conn.instance_variable_get("@connection").execute(sql).each(as: :hash) do |row|
245
+ ret << row
246
+ yield row if block_given?
247
+ end
248
+ end
249
+
250
+ ret
251
+ end
252
+
253
+
254
+ def self.parse_for_string_filter(value)
255
+ value.nil? ? 'NULL' : "'#{value.to_s.gsub('\'','\'\'')}'"
256
+ end
257
+
258
+ def parse_for_string_filter(value)
259
+ self.class.parse_for_string_filter value
260
+ end
261
+
262
+ def self.connection_handler
263
+ @conn_handler ||= const_get('ActiveRecord::Base')
264
+ end
265
+
266
+ def self.get_udf_definition(name)
267
+ execute('SELECT R.ROUTINE_CATALOG AS [catalog], R.ROUTINE_SCHEMA AS [schema], R.ROUTINE_NAME AS [name], ' +
268
+ 'R.ROUTINE_DEFINITION AS [definition] FROM INFORMATION_SCHEMA.ROUTINES R WHERE ' +
269
+ 'R.ROUTINE_TYPE=\'FUNCTION\' AND R.DATA_TYPE=\'TABLE\' AND R.ROUTINE_NAME=@0',
270
+ [
271
+ ['varchar(100)', parse_for_string_filter(name)]
272
+ ]
273
+ ) do |row|
274
+ return [ row['catalog'], row['schema'], row['name'], row['definition'] ]
275
+ end
276
+ [ nil, nil, nil, nil ]
277
+ end
278
+
279
+
280
+ def self.get_udf_params(sql_def)
281
+ # get everything before the return definition
282
+ # should be something like "CREATE FUNCTION xyz (a type, b type)"
283
+ sql_def = sql_def.split(/\sreturn/i)[0]
284
+
285
+ ret = {}
286
+
287
+ if sql_def['(']
288
+ param_defs = sql_def.split('(', 2)[1].rpartition(')')[0].split(',').map{|d| d.strip}
289
+
290
+ param_defs.each_with_index do |raw,idx|
291
+ pname,pdatatype = raw.split(' ')
292
+
293
+ psym = pname
294
+ if psym[0] == '@'
295
+ psym = psym[1..-1]
296
+ end
297
+ psym = psym.underscore.to_sym
298
+
299
+ pdatatype.downcase!
300
+
301
+ if pdatatype.include? 'date'
302
+ pfmt = :parse_for_date_filter
303
+ ptype = :datetime
304
+ elsif pdatatype.include? 'float'
305
+ pfmt = :parse_for_float_filter
306
+ ptype = :float
307
+ elsif pdatatype.include? 'int'
308
+ pfmt = :parse_for_int_filter
309
+ ptype = :integer
310
+ elsif pdatatype.include? 'bit'
311
+ pfmt = :parse_for_boolean_filter
312
+ ptype = :boolean
313
+ else
314
+ pfmt = :parse_for_string_filter
315
+ ptype = :string
316
+ end
317
+
318
+ ret[psym] = { name: pname, data_type: pdatatype, type: ptype, format: pfmt, ordinal: idx }
319
+ end
320
+ end
321
+
322
+ ret
323
+ end
324
+
325
+
326
+ def self.get_udf_columns(catalog, schema, name)
327
+ execute('SELECT C.COLUMN_NAME AS [name], C.IS_NULLABLE AS [nullable], C.DATA_TYPE AS [type], ' +
328
+ 'C.CHARACTER_MAXIMUM_LENGTH AS [length], C.ORDINAL_POSITION AS [ordinal] ' +
329
+ 'FROM INFORMATION_SCHEMA.ROUTINE_COLUMNS C ' +
330
+ 'WHERE C.TABLE_CATALOG=@0 AND C.TABLE_SCHEMA=@1 AND C.TABLE_NAME=@2 ' +
331
+ 'ORDER BY C.ORDINAL_POSITION',
332
+ [
333
+ ['varchar(100)', parse_for_string_filter(catalog)],
334
+ ['varchar(100)', parse_for_string_filter(schema)],
335
+ ['varchar(100)', parse_for_string_filter(name)]
336
+ ]
337
+ ) do |row|
338
+ yield row if block_given?
339
+ end
340
+ end
341
+
342
+
343
+ def self.process_udf(name)
344
+ catalog, schema, name, sql_def = get_udf_definition(name)
345
+
346
+ raise StandardError.new("The specified function '#{name.to_s.gsub('\'','\'\'')}' could not be defined.") if sql_def.blank?
347
+
348
+ @param_info = get_udf_params(sql_def)
349
+
350
+ if @param_info.blank?
351
+ @udf_args = ''
352
+ else
353
+ @udf_args = @param_info.map.with_index { |v,i| "@#{i}" }.join(', ')
354
+ end
355
+
356
+ @column_info = [ ]
357
+
358
+ get_udf_columns(catalog, schema, name) do |column|
359
+ col_key = column['name'].underscore.to_sym
360
+ getter = col_key
361
+ setter = "#{col_key}="
362
+
363
+ col_info = { name: column['name'], key: col_key, ordinal: column['ordinal'], nullable: true, length: -1 }
364
+
365
+ type = column['type'].downcase
366
+
367
+ unless column['length'].blank?
368
+ type += "(#{column['length']})"
369
+ col_info[:length] = column['length'].to_i
370
+ end
371
+
372
+ col_info[:data_type] = type
373
+
374
+ attr_reader col_key
375
+
376
+ if type == 'int' || type == 'integer'
377
+ define_method setter do |value|
378
+ instance_variable_set "@#{col_key}", parse_for_int_column(value)
379
+ end
380
+ col_info[:type] = :integer
381
+ elsif type == 'float'
382
+ define_method setter do |value|
383
+ instance_variable_set "@#{col_key}", parse_for_float_column(value)
384
+ end
385
+ col_info[:type] = :float
386
+ elsif type == 'date' || (type == 'datetime' && col_key.to_s.include?('date'))
387
+ define_method setter do |value|
388
+ instance_variable_set "@#{col_key}", parse_for_date_column(value)
389
+ end
390
+ col_info[:type] = :datetime
391
+ elsif type == 'datetime'
392
+ define_method setter do |value|
393
+ instance_variable_set "@#{col_key}", parse_for_time_column(value)
394
+ end
395
+ col_info[:type] = :datetime
396
+ elsif type == 'bit'
397
+ define_method setter do |value|
398
+ instance_variable_set "@#{col_key}", parse_for_boolean_column(value)
399
+ end
400
+ define_method "#{getter}?" do
401
+ instance_variable_get "@#{col_key}"
402
+ end
403
+ col_info[:type] = :boolean
404
+ else
405
+ define_method setter do |value|
406
+ instance_variable_set "@#{col_key}", value.nil? ? nil : value.to_s
407
+ end
408
+ col_info[:type] = :string
409
+ end
410
+
411
+ if column['nullable'].upcase == 'NO'
412
+ validates col_key, presence: true
413
+ col_info[:nullable] = false
414
+ end
415
+
416
+ @column_info << col_info
417
+ end
418
+
419
+ "[#{schema}].[#{name}]"
420
+ end
421
+
422
+ end
423
+ end