pg.coffee 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. # This is an implementation of the OT data backend for PostgreSQL. It requires
  2. # that you have two tables defined in your schema: one for the snapshots
  3. # and one for the operations. You must also install the 'pg' package.
  4. #
  5. #
  6. # Example usage:
  7. #
  8. # var connect = require('connect');
  9. # var share = require('share').server;
  10. #
  11. # var server = connect(connect.logger());
  12. #
  13. # var options = {
  14. # db: {
  15. # type: 'pg',
  16. # uri: 'tcp://josh:@localhost/sharejs',
  17. # create_tables_automatically: true
  18. # }
  19. # };
  20. #
  21. # share.attach(server, options);
  22. # server.listen(9000);
  23. #
  24. # You can run bin/setup_pg to create the SQL tables initially.
  25. pg = require('pg').native
  26. defaultOptions =
  27. schema: 'sharejs'
  28. create_tables_automatically: true
  29. operations_table: 'ops'
  30. snapshot_table: 'snapshots'
  31. module.exports = PgDb = (options) ->
  32. return new Db if !(this instanceof PgDb)
  33. options ?= {}
  34. options[k] ?= v for k, v of defaultOptions
  35. client = new pg.Client options.uri
  36. client.connect()
  37. snapshot_table = options.schema and "#{options.schema}.#{options.snapshot_table}" or options.snapshot_table
  38. operations_table = options.schema and "#{options.schema}.#{options.operations_table}" or options.operations_table
  39. @close = ->
  40. client.end()
  41. @initialize = (callback) ->
  42. console.warn 'Creating postgresql database tables'
  43. sql = """
  44. CREATE SCHEMA #{options.schema};
  45. CREATE TABLE #{snapshot_table} (
  46. doc text NOT NULL,
  47. v int4 NOT NULL,
  48. type text NOT NULL,
  49. snapshot text NOT NULL,
  50. meta text NOT NULL,
  51. created_at timestamp(6) NOT NULL,
  52. CONSTRAINT snapshots_pkey PRIMARY KEY (doc, v)
  53. );
  54. CREATE TABLE #{operations_table} (
  55. doc text NOT NULL,
  56. v int4 NOT NULL,
  57. op text NOT NULL,
  58. meta text NOT NULL,
  59. CONSTRAINT operations_pkey PRIMARY KEY (doc, v)
  60. );
  61. """
  62. client.query sql, (error, result) ->
  63. callback? error?.message
  64. # This will perminantly delete all data in the database.
  65. @dropTables = (callback) ->
  66. sql = "DROP SCHEMA #{options.schema} CASCADE;"
  67. client.query sql, (error, result) ->
  68. callback? error.message
  69. @create = (docName, docData, callback) ->
  70. sql = """
  71. INSERT INTO #{snapshot_table} ("doc", "v", "snapshot", "meta", "type", "created_at")
  72. VALUES ($1, $2, $3, $4, $5, now())
  73. """
  74. values = [docName, docData.v, JSON.stringify(docData.snapshot), JSON.stringify(docData.meta), docData.type]
  75. client.query sql, values, (error, result) ->
  76. if !error?
  77. callback?()
  78. else if error.toString().match "duplicate key value violates unique constraint"
  79. callback? "Document already exists"
  80. else
  81. callback? error?.message
  82. @delete = (docName, dbMeta, callback) ->
  83. sql = """
  84. DELETE FROM #{operations_table}
  85. WHERE "doc" = $1
  86. RETURNING *
  87. """
  88. values = [docName]
  89. client.query sql, values, (error, result) ->
  90. if !error?
  91. sql = """
  92. DELETE FROM #{snapshot_table}
  93. WHERE "doc" = $1
  94. RETURNING *
  95. """
  96. client.query sql, values, (error, result) ->
  97. if !error? and result.rows.length > 0
  98. callback?()
  99. else if !error?
  100. callback? "Document does not exist"
  101. else
  102. callback? error?.message
  103. else
  104. callback? error?.message
  105. @getSnapshot = (docName, callback) ->
  106. sql = """
  107. SELECT *
  108. FROM #{snapshot_table}
  109. WHERE "doc" = $1
  110. ORDER BY "v" DESC
  111. LIMIT 1
  112. """
  113. values = [docName]
  114. client.query sql, values, (error, result) ->
  115. if !error? and result.rows.length > 0
  116. row = result.rows[0]
  117. data =
  118. v: row.v
  119. snapshot: JSON.parse(row.snapshot)
  120. meta: JSON.parse(row.meta)
  121. type: row.type
  122. callback? null, data
  123. else if !error?
  124. callback? "Document does not exist"
  125. else
  126. callback? error?.message
  127. @writeSnapshot = (docName, docData, dbMeta, callback) ->
  128. sql = """
  129. UPDATE #{snapshot_table}
  130. SET "v" = $2, "snapshot" = $3, "meta" = $4
  131. WHERE "doc" = $1
  132. """
  133. values = [docName, docData.v, JSON.stringify(docData.snapshot), JSON.stringify(docData.meta)]
  134. client.query sql, values, (error, result) ->
  135. if !error?
  136. callback?()
  137. else
  138. callback? error?.message
  139. @getOps = (docName, start, end, callback) ->
  140. end = if end? then end - 1 else 2147483647
  141. sql = """
  142. SELECT *
  143. FROM #{operations_table}
  144. WHERE "v" BETWEEN $1 AND $2
  145. AND "doc" = $3
  146. ORDER BY "v" ASC
  147. """
  148. values = [start, end, docName]
  149. client.query sql, values, (error, result) ->
  150. if !error?
  151. data = result.rows.map (row) ->
  152. return {
  153. op: JSON.parse row.op
  154. # v: row.version
  155. meta: JSON.parse row.meta
  156. }
  157. callback? null, data
  158. else
  159. callback? error?.message
  160. @writeOp = (docName, opData, callback) ->
  161. sql = """
  162. INSERT INTO #{operations_table} ("doc", "op", "v", "meta")
  163. VALUES ($1, $2, $3, $4)
  164. """
  165. values = [docName, JSON.stringify(opData.op), opData.v, JSON.stringify(opData.meta)]
  166. client.query sql, values, (error, result) ->
  167. if !error?
  168. callback?()
  169. else
  170. callback? error?.message
  171. # Immediately try and create the database tables if need be. Its possible that a query
  172. # which happens immediately will happen before the database has been initialized.
  173. #
  174. # But, its not really a big problem.
  175. if options.create_tables_automatically
  176. client.query "SELECT * from #{snapshot_table} LIMIT 0", (error, result) =>
  177. @initialize() if error?.message.match "does not exist"
  178. this